byuckchon-frontend-cli 1.9.5 → 1.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,13 +11,15 @@ import {
11
11
  apiRootForFramework,
12
12
  scaffoldApiConventionDoc,
13
13
  } from '../generators/apiConventionDoc.js';
14
+ import { scaffoldReviewAutomation } from '../generators/scaffoldReviewAutomation.js';
15
+ import { detectMonorepoRoot } from '../context/monorepo.js';
14
16
  import { ensureRequiredDependencies } from '../utils/ensureRequiredDependencies.js';
15
17
 
16
18
  /**
17
19
  * `bc adopt`
18
20
  *
19
- * 기존 프로젝트(현재 디렉터리)에 bc.config.json 필수 의존성을 추가한다.
20
- * 기존 소스 코드는 건드리지 않는다.
21
+ * 기존 프로젝트(현재 디렉터리)에 bc.config.json, 필수 의존성, PR 리뷰 자동화를 추가한다.
22
+ * 기존 소스 코드와 이미 존재하는 tools/workflow는 건드리지 않는다.
21
23
  *
22
24
  * 흐름:
23
25
  * 1) 자동 감지(framework, styling, language, routing 등) 결과를 보여주고
@@ -176,6 +178,39 @@ export async function adoptCommand(opts = {}) {
176
178
  } catch {
177
179
  /* 문서 스캐폴드 실패는 치명적이지 않음 */
178
180
  }
181
+
182
+ // PR 리뷰 자동화는 모노레포 루트에 한 번만 둔다. 기존 tools/workflow는 덮어쓰지 않는다.
183
+ try {
184
+ const monorepoRoot = await detectMonorepoRoot(cwd);
185
+ const reviewProjectRoot = monorepoRoot ?? cwd;
186
+ const projectType = monorepoRoot ? 'monorepo' : 'single';
187
+ const reviewAutomation = await scaffoldReviewAutomation({
188
+ projectRoot: reviewProjectRoot,
189
+ projectType,
190
+ overwrite: false,
191
+ });
192
+
193
+ if (reviewAutomation.workflows.length) {
194
+ console.log(
195
+ chalk.green(
196
+ ` ✓ ESLint Convention Review 설정 추가: ${path.join(reviewProjectRoot, '.github', 'workflows')}`,
197
+ ),
198
+ );
199
+ } else {
200
+ console.log(
201
+ chalk.dim(
202
+ ` ESLint Convention Review 설정 유지: ${path.join(reviewProjectRoot, '.github', 'workflows')} (이미 존재)`,
203
+ ),
204
+ );
205
+ }
206
+ } catch (error) {
207
+ console.log(
208
+ chalk.yellow(
209
+ ` ⚠ ESLint Convention Review 설정을 추가하지 못했습니다: ${error.message}`,
210
+ ),
211
+ );
212
+ }
213
+
179
214
  console.log();
180
215
  console.log(chalk.dim(' 다음:'));
181
216
  if (!process.env.ANTHROPIC_API_KEY) {
@@ -21,6 +21,7 @@ export const versions = {
21
21
  // State & Data
22
22
  zustand: '^5.0.3',
23
23
  axios: '^1.8.4',
24
+ 'react-router-dom': '^7.5.0',
24
25
  '@tanstack/react-query': '^5.74.4',
25
26
 
26
27
  // Styling
@@ -27,7 +27,10 @@ export async function createApp({ appDir, config, scope }) {
27
27
 
28
28
  await createFolders(appDir, config);
29
29
  await createBaseFiles(appDir, config);
30
- await scaffoldApiConventionDoc({ projectRoot: appDir, framework: config.framework });
30
+ await scaffoldApiConventionDoc({
31
+ projectRoot: appDir,
32
+ framework: config.framework,
33
+ });
31
34
  await createBcConfig(appDir, config);
32
35
 
33
36
  await createAppPackageJson(appDir, config, scope);
@@ -43,8 +46,8 @@ async function rm(target) {
43
46
  }
44
47
 
45
48
  /**
46
- * 워크스페이스 앱용 package.json. 모노레포에서는 React 버전을 하나로 통일하려고
47
- * (pnpm hoist 충돌 방지) react/react-dom 모두 19 계열로 맞춘다.
49
+ * 워크스페이스 앱용 package.json.
50
+ * 단일 프로젝트와 동일하게 Vite React는 React 18, Next는 React 19 사용한다.
48
51
  */
49
52
  async function createAppPackageJson(appDir, config, scope) {
50
53
  const isReact = config.framework === 'react';
@@ -77,13 +80,14 @@ async function createAppPackageJson(appDir, config, scope) {
77
80
  type: 'module',
78
81
  scripts,
79
82
  dependencies: {
80
- react: versions['next-react'],
81
- 'react-dom': versions['next-react-dom'],
83
+ react: isReact ? versions.react : versions['next-react'],
84
+ 'react-dom': isReact ? versions['react-dom'] : versions['next-react-dom'],
82
85
  ...(isReact ? {} : { next: versions.next }),
83
86
  zustand: versions.zustand,
84
87
  ...(isReact
85
88
  ? {
86
89
  axios: versions.axios,
90
+ 'react-router-dom': versions['react-router-dom'],
87
91
  '@tanstack/react-query': versions['@tanstack/react-query'],
88
92
  }
89
93
  : {}),
@@ -118,7 +122,10 @@ async function createAppPackageJson(appDir, config, scope) {
118
122
  },
119
123
  };
120
124
 
121
- await write(path.join(appDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
125
+ await write(
126
+ path.join(appDir, 'package.json'),
127
+ JSON.stringify(pkg, null, 2) + '\n'
128
+ );
122
129
  }
123
130
 
124
131
  /**
@@ -154,7 +161,7 @@ export default [
154
161
  },
155
162
  },
156
163
  ];
157
- `,
164
+ `
158
165
  );
159
166
 
160
167
  if (isReact) {
@@ -182,8 +189,8 @@ export default [
182
189
  include: ['src', 'vite.config.ts'],
183
190
  },
184
191
  null,
185
- 2,
186
- ) + '\n',
192
+ 2
193
+ ) + '\n'
187
194
  );
188
195
  } else {
189
196
  await write(
@@ -204,8 +211,8 @@ export default [
204
211
  exclude: ['node_modules'],
205
212
  },
206
213
  null,
207
- 2,
208
- ) + '\n',
214
+ 2
215
+ ) + '\n'
209
216
  );
210
217
  }
211
218
  }
@@ -424,8 +424,9 @@ export default defineConfig({
424
424
  // src/App.css
425
425
  await write(
426
426
  path.join(rootDir, "src/App.css"),
427
- `@import "./tokens.css";
428
- @import 'tailwindcss';
427
+ `@import 'tailwindcss';
428
+ @import "@byuckchon-frontend/settings/motion";
429
+ @import "./tokens.css";
429
430
  `
430
431
  );
431
432
  await write(path.join(rootDir, "src/tokens.css"), "");
@@ -593,8 +594,9 @@ export default config;
593
594
  // src/app/globals.css
594
595
  await write(
595
596
  path.join(rootDir, "src/app/globals.css"),
596
- `@import "../tokens.css";
597
- @import 'tailwindcss';
597
+ `@import 'tailwindcss';
598
+ @import "@byuckchon-frontend/settings/motion";
599
+ @import "../tokens.css";
598
600
  `
599
601
  );
600
602
  await write(path.join(rootDir, "token.config.js"), TOKEN_CONFIG_JS);
@@ -3,7 +3,7 @@ import path from 'path';
3
3
 
4
4
  /**
5
5
  * React 폴더 구조
6
- * lib → store → api → hooks → context → components → layoutpage
6
+ * lib → store → api → hooks → context → components → layoutspages
7
7
  * assets는 모든 레이어에서 참조 가능
8
8
  */
9
9
  const REACT_FOLDERS = [
@@ -17,8 +17,8 @@ const REACT_FOLDERS = [
17
17
  'src/hooks',
18
18
  'src/context',
19
19
  'src/components',
20
- 'src/layout',
21
- 'src/page',
20
+ 'src/layouts',
21
+ 'src/pages',
22
22
  ];
23
23
 
24
24
  /**
@@ -58,8 +58,8 @@ export async function createFolders(rootDir, config) {
58
58
  await writeFile(path.join(rootDir, 'src/store/index.ts'), '// Zustand 스토어\n');
59
59
  await writeFile(path.join(rootDir, 'src/api/index.ts'), '// Axios API 호출\n');
60
60
  await writeFile(path.join(rootDir, 'src/context/index.tsx'), '// React Context\n');
61
- await writeFile(path.join(rootDir, 'src/layout/index.tsx'), '// 레이아웃 컴포넌트\n');
62
- await writeFile(path.join(rootDir, 'src/page/index.tsx'), '// 페이지 컴포넌트\n');
61
+ await writeFile(path.join(rootDir, 'src/layouts/index.tsx'), '// 레이아웃 컴포넌트\n');
62
+ await writeFile(path.join(rootDir, 'src/pages/index.tsx'), '// 페이지 컴포넌트\n');
63
63
  } else {
64
64
  await writeFile(path.join(rootDir, 'src/constant/index.ts'), '// 상수 정의\n');
65
65
  await writeFile(path.join(rootDir, 'src/providers/index.tsx'), '// 전역 Provider\n');
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
 
4
4
  import { createApp } from './createApp.js';
5
+ import { scaffoldReviewAutomation } from './scaffoldReviewAutomation.js';
5
6
 
6
7
  /**
7
8
  * 새 pnpm 모노레포를 스캐폴드한다. (marketd-frontend 구조 참고)
@@ -32,6 +33,7 @@ export async function createMonorepo(config) {
32
33
  await writeRootFiles(root, config, scope);
33
34
  await writeConfigTypescript(root, scope);
34
35
  await writeConfigEslint(root, scope);
36
+ await scaffoldReviewAutomation({ projectRoot: root, projectType: 'monorepo' });
35
37
 
36
38
  // 최초 앱 하나만 생성. 이후 추가는 `bc add`.
37
39
  const appName = config.appName;
@@ -71,9 +73,9 @@ async function writeRootFiles(root, config, scope) {
71
73
  typecheck: 'turbo run typecheck',
72
74
  'tokens:build': 'turbo run tokens:build',
73
75
  format:
74
- 'prettier --write "**/*.{ts,tsx,js,jsx,json,md,yml,yaml}" --ignore-path .gitignore',
76
+ 'prettier --write "**/*.{ts,tsx,js,jsx,cjs,json,md,yml,yaml}" --ignore-path .gitignore',
75
77
  'format:check':
76
- 'prettier --check "**/*.{ts,tsx,js,jsx,json,md,yml,yaml}" --ignore-path .gitignore',
78
+ 'prettier --check "**/*.{ts,tsx,js,jsx,cjs,json,md,yml,yaml}" --ignore-path .gitignore',
77
79
  clean: 'turbo run clean && rm -rf node_modules .turbo',
78
80
  preinstall: 'npx only-allow pnpm',
79
81
  // 최초 앱 실행 단축키 (bc add 시 앱마다 추가됨).
@@ -18,7 +18,10 @@ export async function createPackageJson(rootDir, config) {
18
18
  'tokens:build': 'style-dictionary build --config token.config.js',
19
19
  lint: 'eslint . --ext ts,tsx',
20
20
  preview: 'vite preview',
21
- format: 'prettier --write "src/**/*.{ts,tsx,css}"',
21
+ format:
22
+ 'prettier --write "src/**/*.{ts,tsx,css}" "tools/**/*.{js,cjs,json,md}"',
23
+ 'format:check':
24
+ 'prettier --check "src/**/*.{ts,tsx,css}" "tools/**/*.{js,cjs,json,md}"',
22
25
  }
23
26
  : {
24
27
  dev: 'next dev',
@@ -26,7 +29,10 @@ export async function createPackageJson(rootDir, config) {
26
29
  'tokens:build': 'style-dictionary build --config token.config.js',
27
30
  start: 'next start',
28
31
  lint: 'next lint',
29
- format: 'prettier --write "src/**/*.{ts,tsx,css}"',
32
+ format:
33
+ 'prettier --write "src/**/*.{ts,tsx,css}" "tools/**/*.{js,cjs,json,md}"',
34
+ 'format:check':
35
+ 'prettier --check "src/**/*.{ts,tsx,css}" "tools/**/*.{js,cjs,json,md}"',
30
36
  },
31
37
  dependencies: {
32
38
  react: isReact ? versions.react : versions['next-react'],
@@ -37,6 +43,7 @@ export async function createPackageJson(rootDir, config) {
37
43
  ...(isReact
38
44
  ? {
39
45
  axios: versions.axios,
46
+ 'react-router-dom': versions['react-router-dom'],
40
47
  }
41
48
  : {}),
42
49
  zod: versions.zod,
@@ -1,22 +1,18 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { exec as execCallback } from 'child_process';
4
- import { promisify } from 'util';
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { exec as execCallback } from "child_process";
4
+ import { promisify } from "util";
5
5
 
6
- import { createBaseFiles } from './createBaseFiles.js';
7
- import { createBcConfig } from './createBcConfig.js';
8
- import { createFolders } from './createFolders.js';
9
- import { createPackageJson } from './createPackageJson.js';
10
- import { createReadme } from './createReadme.js';
11
- import { scaffoldApiConventionDoc } from './apiConventionDoc.js';
6
+ import { createBaseFiles } from "./createBaseFiles.js";
7
+ import { createBcConfig } from "./createBcConfig.js";
8
+ import { createFolders } from "./createFolders.js";
9
+ import { createPackageJson } from "./createPackageJson.js";
10
+ import { createReadme } from "./createReadme.js";
11
+ import { scaffoldApiConventionDoc } from "./apiConventionDoc.js";
12
+ import { scaffoldReviewAutomation } from "./scaffoldReviewAutomation.js";
13
+ import { BYUCKCHON_PACKAGES } from "./install.js";
12
14
 
13
15
  const exec = promisify(execCallback);
14
- const BYUCKCHON_PACKAGES = [
15
- '@byuckchon-frontend/hooks',
16
- '@byuckchon-frontend/utils',
17
- '@byuckchon-frontend/basic-ui',
18
- '@byuckchon-frontend/core',
19
- ];
20
16
 
21
17
  export async function createProject(config) {
22
18
  const rootDir = path.resolve(config.projectName);
@@ -27,10 +23,17 @@ export async function createProject(config) {
27
23
  await createBaseFiles(rootDir, config);
28
24
  await createReadme(rootDir, config);
29
25
  // API 코드 컨벤션 .md 를 프레임워크에 맞는 API 루트(src/api | src/lib/api)에 깐다.
30
- await scaffoldApiConventionDoc({ projectRoot: rootDir, framework: config.framework });
26
+ await scaffoldApiConventionDoc({
27
+ projectRoot: rootDir,
28
+ framework: config.framework,
29
+ });
31
30
  await createBcConfig(rootDir, config);
31
+ await scaffoldReviewAutomation({
32
+ projectRoot: rootDir,
33
+ projectType: "single",
34
+ });
32
35
 
33
36
  // 최신 버전(latest 포함) 의존성을 실제로 설치해 lockfile까지 생성
34
- await exec('npm install', { cwd: rootDir });
35
- await exec(`npm install ${BYUCKCHON_PACKAGES.join(' ')}`, { cwd: rootDir });
37
+ await exec("npm install", { cwd: rootDir });
38
+ await exec(`npm install ${BYUCKCHON_PACKAGES.join(" ")}`, { cwd: rootDir });
36
39
  }
@@ -10,7 +10,7 @@ export async function createReadme(rootDir, config) {
10
10
 
11
11
  | 항목 | 내용 |
12
12
  |------|------|
13
- | Framework | ${isReact ? 'React 19' : 'Next.js 15 (App Router)'} |
13
+ | Framework | ${isReact ? 'React 18' : 'Next.js 15 (App Router)'} |
14
14
  | Language | TypeScript |
15
15
  | Build Tool | ${isReact ? 'Vite' : 'Next.js built-in'} |
16
16
  | Styling | Tailwind CSS |
@@ -47,8 +47,8 @@ src/
47
47
  ├── components/ 재사용 가능한 UI 컴포넌트${
48
48
  isReact
49
49
  ? `
50
- ├── layout/ 레이아웃 컴포넌트
51
- └── page/ 페이지 컴포넌트`
50
+ ├── layouts/ 레이아웃 컴포넌트
51
+ └── pages/ 페이지 컴포넌트`
52
52
  : `
53
53
  └── app/ Next.js App Router (layout, page 등)`
54
54
  }
@@ -61,7 +61,7 @@ ${
61
61
  각 레이어는 아래 방향으로만 import 해야 합니다.
62
62
 
63
63
  \`\`\`
64
- lib → store → api → hooks → context → components → layoutpage
64
+ lib → store → api → hooks → context → components → layoutspages
65
65
  \`\`\`
66
66
 
67
67
  \`assets\`는 모든 레이어에서 자유롭게 참조 가능합니다.`
@@ -10,6 +10,7 @@ export const BYUCKCHON_PACKAGES = [
10
10
  '@byuckchon-frontend/utils',
11
11
  '@byuckchon-frontend/basic-ui',
12
12
  '@byuckchon-frontend/core',
13
+ '@byuckchon-frontend/settings',
13
14
  ];
14
15
 
15
16
  async function hasPnpm() {
@@ -0,0 +1,105 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const TEMPLATE_ROOT = path.resolve(
6
+ path.dirname(fileURLToPath(import.meta.url)),
7
+ '../../templates/review-automation',
8
+ );
9
+
10
+ const ESLINT_WORKFLOW_BY_PROJECT_TYPE = {
11
+ single: 'eslint-convention-review.single.yml',
12
+ monorepo: 'eslint-convention-review.monorepo.yml',
13
+ };
14
+
15
+ function shouldCopyTemplateFile(source) {
16
+ return path.basename(source) !== '.DS_Store';
17
+ }
18
+
19
+ async function pathExists(target) {
20
+ try {
21
+ await fs.access(target);
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ }
27
+
28
+ async function copyFileIfAllowed(source, target, overwrite) {
29
+ if (!overwrite && (await pathExists(target))) {
30
+ return false;
31
+ }
32
+
33
+ await fs.copyFile(source, target);
34
+ return true;
35
+ }
36
+
37
+ /**
38
+ * 프로젝트 루트에 PR 리뷰 자동화 파일을 생성한다.
39
+ *
40
+ * - tools/ 는 ESLint convention rule 및 PR 댓글 게시 스크립트를 제공한다.
41
+ * - .github/workflows/ 는 프로젝트 유형에 맞는 ESLint workflow 하나와
42
+ * 공통 workflow(AI Code Review, PR Check 등)를 제공한다.
43
+ *
44
+ * @param {object} args
45
+ * @param {string} args.projectRoot 새로 생성한 프로젝트의 절대 경로
46
+ * @param {'single' | 'monorepo'} args.projectType 생성할 프로젝트 유형
47
+ * @param {boolean} [args.overwrite=true] 기존 파일을 템플릿으로 덮어쓸지
48
+ * @returns {Promise<{ toolsDir: string, workflows: string[] }>}
49
+ */
50
+ export async function scaffoldReviewAutomation({
51
+ projectRoot,
52
+ projectType,
53
+ overwrite = true,
54
+ }) {
55
+ const eslintWorkflow = ESLINT_WORKFLOW_BY_PROJECT_TYPE[projectType];
56
+
57
+ if (!eslintWorkflow) {
58
+ throw new Error(`지원하지 않는 프로젝트 유형입니다: ${projectType}`);
59
+ }
60
+
61
+ const toolsTemplateDir = path.join(TEMPLATE_ROOT, 'tools');
62
+ const workflowsTemplateDir = path.join(TEMPLATE_ROOT, 'github', 'workflows');
63
+ const toolsTargetDir = path.join(projectRoot, 'tools');
64
+ const workflowsTargetDir = path.join(projectRoot, '.github', 'workflows');
65
+
66
+ await fs.cp(toolsTemplateDir, toolsTargetDir, {
67
+ recursive: true,
68
+ force: overwrite,
69
+ errorOnExist: false,
70
+ filter: shouldCopyTemplateFile,
71
+ });
72
+ await fs.mkdir(workflowsTargetDir, { recursive: true });
73
+
74
+ const workflowFiles = await fs.readdir(workflowsTemplateDir);
75
+ const commonWorkflows = workflowFiles.filter(
76
+ (fileName) =>
77
+ fileName.endsWith('.yml') &&
78
+ !fileName.startsWith('eslint-convention-review.'),
79
+ );
80
+ const copiedWorkflows = [];
81
+
82
+ for (const fileName of commonWorkflows) {
83
+ const copied = await copyFileIfAllowed(
84
+ path.join(workflowsTemplateDir, fileName),
85
+ path.join(workflowsTargetDir, fileName),
86
+ overwrite,
87
+ );
88
+ if (copied) copiedWorkflows.push(fileName);
89
+ }
90
+
91
+ const eslintWorkflowTarget = path.join(
92
+ workflowsTargetDir,
93
+ 'eslint-convention-review.yml',
94
+ );
95
+ const copiedEslintWorkflow = await copyFileIfAllowed(
96
+ path.join(workflowsTemplateDir, eslintWorkflow),
97
+ eslintWorkflowTarget,
98
+ overwrite,
99
+ );
100
+ if (copiedEslintWorkflow) {
101
+ copiedWorkflows.push('eslint-convention-review.yml');
102
+ }
103
+
104
+ return { toolsDir: toolsTargetDir, workflows: copiedWorkflows };
105
+ }
@@ -0,0 +1,86 @@
1
+ # ESLint 기반 기계적 컨벤션을 PR 인라인 댓글로 게시합니다.
2
+ # typecheck/lint/build 성공 여부는 PR Check workflow가 담당합니다.
3
+ #
4
+ # 각 패키지의 eslint.config.mjs에는 internal 플러그인이 없으므로,
5
+ # tools/eslint-rules/review.config.mjs 전용 config로 레포 루트에서
6
+ # PR 변경 파일만 한 번에 검사합니다.
7
+
8
+ name: ESLint Convention Review
9
+
10
+ on:
11
+ pull_request:
12
+ types: [opened, synchronize, reopened]
13
+ branches:
14
+ - dev
15
+
16
+ concurrency:
17
+ group: eslint-convention-review-${{ github.event.pull_request.number }}
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ eslint-conventions:
22
+ name: ESLint Convention Review
23
+ runs-on: ubuntu-latest
24
+ permissions:
25
+ contents: read
26
+ pull-requests: write
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+
30
+ - uses: pnpm/action-setup@v4
31
+
32
+ - uses: actions/setup-node@v4
33
+ with:
34
+ node-version-file: .nvmrc
35
+ cache: pnpm
36
+
37
+ - name: 의존성 설치
38
+ run: pnpm install --frozen-lockfile
39
+
40
+ - name: PR 변경 파일 조회
41
+ env:
42
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43
+ run: |
44
+ gh api "repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
45
+ --paginate \
46
+ --jq '.[] | select(.status != "removed") | .filename | select(test("\\.(ts|tsx)$"))' \
47
+ > /tmp/eslint-convention-files.txt
48
+
49
+ echo "변경된 TypeScript 파일:"
50
+ cat /tmp/eslint-convention-files.txt
51
+
52
+ - name: ESLint 컨벤션 검사
53
+ run: |
54
+ if [[ ! -s /tmp/eslint-convention-files.txt ]]; then
55
+ echo "변경된 TypeScript 파일이 없습니다."
56
+ printf '{"source":{"name":"eslint-conventions"},"diagnostics":[]}' \
57
+ > /tmp/eslint-conventions.rdjson
58
+ exit 0
59
+ fi
60
+
61
+ mapfile -t changed_files < /tmp/eslint-convention-files.txt
62
+
63
+ set +e
64
+ pnpm exec eslint "${changed_files[@]}" \
65
+ --config tools/eslint-rules/review.config.mjs \
66
+ --no-error-on-unmatched-pattern \
67
+ -f tools/eslint-rules/internal-rdjson-formatter.js \
68
+ -o /tmp/eslint-conventions.rdjson
69
+ eslint_exit=$?
70
+ set -e
71
+
72
+ if [[ ! -f /tmp/eslint-conventions.rdjson ]]; then
73
+ echo "ESLint가 /tmp/eslint-conventions.rdjson 을 생성하지 못했습니다. (exit ${eslint_exit})"
74
+ exit "$eslint_exit"
75
+ fi
76
+
77
+ - name: 리뷰 댓글 게시
78
+ env:
79
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80
+ PR_NUMBER: ${{ github.event.pull_request.number }}
81
+ REPO: ${{ github.repository }}
82
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
83
+ run: |
84
+ node tools/post-eslint-review-comments.cjs \
85
+ /tmp/eslint-conventions.rdjson \
86
+ eslint-conventions
@@ -0,0 +1,44 @@
1
+ name: ESLint Convention Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened]
6
+ branches:
7
+ - dev
8
+
9
+ concurrency:
10
+ group: eslint-convention-review-${{ github.event.pull_request.number }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ eslint-conventions:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: read
18
+ pull-requests: write
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 22
25
+ cache: npm
26
+
27
+ - name: 의존성 설치
28
+ run: npm ci
29
+
30
+ - name: ESLint 컨벤션 리뷰 댓글
31
+ env:
32
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33
+ PR_NUMBER: ${{ github.event.pull_request.number }}
34
+ REPO: ${{ github.repository }}
35
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
36
+ run: |
37
+ npx eslint . --ext ts,tsx --rulesdir tools/eslint-rules \
38
+ --rule 'internal-blocking-conventions:error' \
39
+ --rule 'internal-warning-conventions:warn' \
40
+ -f tools/eslint-rules/internal-rdjson-formatter.js \
41
+ -o /tmp/eslint-conventions.rdjson || true
42
+ node tools/post-eslint-review-comments.cjs \
43
+ /tmp/eslint-conventions.rdjson \
44
+ eslint-conventions