-
Notifications
You must be signed in to change notification settings - Fork 0
[CI] PR 번들 사이즈 자동 분석 워크플로우 추가 #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
c65dbda
ci(root): PR 번들 사이즈 자동 분석 워크플로우 추가 (#15)
kimminna 5d953e3
fix(ui): 디자인 시스템 패키지명 변경 사항 적용 (#15)
kimminna 742793f
ci(root): Turbo TUI 비활성화 및 Node 24로 업그레이드 (#15)
kimminna 80ae4f5
ci(root): turbo 플래그 --log-output을 --output-logs로 수정 (#15)
kimminna 4f5ab0b
ci(root): 번들 크기 분석을 .next 디렉토리 파싱으로 전환 (#15)
kimminna 84d42a0
ci(root): Turbopack 빌드용 routes-manifest 기반 분석으로 전환 (#15)
kimminna c1a10d8
ci(root): gzip 크기 측정 및 내부 라우트 필터링 (#15)
kimminna 6d1ed19
ci(root): 번들 사이즈 코멘트를 삭제 후 재생성에서 업데이트 방식으로 변경 (#15)
kimminna 49c9f13
Merge branch 'develop' of https://github.com/Team-Timo/Timo-client in…
kimminna 9811b76
ci(root): Debug 스텝을 빌드 실패 시에만 실행되도록 수정 (#15)
kimminna eae6013
fix(root): PR 템플릿 파일명 오타 수정 (#15)
kimminna ff5fe7c
Merge branch 'develop' of https://github.com/Team-Timo/Timo-client in…
kimminna 833dcdd
fix(root): label-by-files와 label-pr 간 라벨 충돌 수정 (#15)
kimminna c8708c8
fix(root): listFiles 페이지네이션 누락으로 인한 라벨 보호 오류 수정 (#15)
kimminna 6bb845b
fix(root): safeGzipSize 오류 무시 제거로 번들 분석 실패 명시화 (#15)
kimminna File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
File renamed without changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| 'use strict'; | ||
|
|
||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const zlib = require('zlib'); | ||
|
|
||
| const BUILD_DIR = path.join(process.cwd(), 'apps/timo-web/.next'); | ||
|
|
||
| const formatBytes = (bytes) => { | ||
| if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(2)} MB`; | ||
| if (bytes >= 1024) return `${(bytes / 1024).toFixed(2)} kB`; | ||
| return `${bytes} B`; | ||
| }; | ||
|
|
||
| const toKb = (bytes) => bytes / 1024; | ||
|
|
||
| const safeGzipSize = (filePath) => { | ||
| try { | ||
| const content = fs.readFileSync(filePath); | ||
| return zlib.gzipSync(content).length; | ||
| } catch (err) { | ||
| throw new Error(`번들 파일 처리 실패: ${filePath} — ${err.message}`); | ||
| } | ||
| }; | ||
|
|
||
| const readJson = (filePath) => { | ||
| try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; } | ||
| }; | ||
|
|
||
| const sumDirGzipSize = (dirPath) => { | ||
| if (!fs.existsSync(dirPath)) return 0; | ||
| return fs.readdirSync(dirPath).reduce((sum, file) => { | ||
| const full = path.join(dirPath, file); | ||
| const stat = fs.statSync(full); | ||
| return sum + (stat.isFile() ? safeGzipSize(full) : 0); | ||
| }, 0); | ||
| }; | ||
|
|
||
| const analyzeBuild = (buildDir) => { | ||
| if (!fs.existsSync(buildDir)) return { routes: [], sharedSize: null }; | ||
|
|
||
| const buildManifest = readJson(path.join(buildDir, 'build-manifest.json')); | ||
| const routesManifest = readJson(path.join(buildDir, 'routes-manifest.json')); | ||
| // webpack 빌드에서만 생성됨 | ||
| const appBuildManifest = readJson(path.join(buildDir, 'app-build-manifest.json')); | ||
|
|
||
| if (!buildManifest) return { routes: [], sharedSize: null }; | ||
|
|
||
| // 내부 Next.js 라우트 필터 (사용자 정의 라우트가 아님) | ||
| const INTERNAL_ROUTES = new Set(['/_not-found', '/_global-error']); | ||
|
|
||
| // 공유 번들 gzip 크기 (rootMainFiles) | ||
| const rootMainFiles = buildManifest.rootMainFiles ?? []; | ||
| const sharedBytes = rootMainFiles.reduce( | ||
| (sum, chunk) => sum + safeGzipSize(path.join(buildDir, chunk)), | ||
| 0 | ||
| ); | ||
|
|
||
| let routes = []; | ||
|
|
||
| if (appBuildManifest) { | ||
| // webpack 모드: app-build-manifest.json으로 라우트별 청크 매핑 | ||
| const pages = appBuildManifest.pages ?? {}; | ||
| routes = Object.entries(pages) | ||
| .filter(([route]) => !INTERNAL_ROUTES.has(route)) | ||
| .map(([route, chunks]) => { | ||
| const pageBytes = chunks.reduce( | ||
| (sum, chunk) => sum + safeGzipSize(path.join(buildDir, chunk)), | ||
| 0 | ||
| ); | ||
| const firstLoadBytes = pageBytes + sharedBytes; | ||
| return { | ||
| path: route, | ||
| size: formatBytes(pageBytes), | ||
| firstLoad: formatBytes(firstLoadBytes), | ||
| firstLoadKb: toKb(firstLoadBytes), | ||
| }; | ||
| }); | ||
| } else { | ||
| // Turbopack 모드: routes-manifest.json으로 라우트 목록 구성 | ||
| const staticRoutes = [ | ||
| ...(routesManifest?.staticRoutes ?? []), | ||
| ...(routesManifest?.dynamicRoutes ?? []), | ||
| ]; | ||
|
|
||
| const appChunksDir = path.join(buildDir, 'static', 'chunks', 'app'); | ||
|
|
||
| routes = staticRoutes | ||
| .filter(({ page }) => !INTERNAL_ROUTES.has(page)) | ||
| .map(({ page }) => { | ||
| const segment = page === '/' ? '' : page; | ||
| const pageChunkDir = path.join(appChunksDir, segment); | ||
| const pageBytes = sumDirGzipSize(pageChunkDir); | ||
|
|
||
| const firstLoadBytes = pageBytes + sharedBytes; | ||
| return { | ||
| path: page, | ||
| size: formatBytes(pageBytes), | ||
| firstLoad: formatBytes(firstLoadBytes), | ||
| firstLoadKb: toKb(firstLoadBytes), | ||
| }; | ||
| }); | ||
|
|
||
| // 라우트가 없으면 공유 번들만이라도 / 라우트로 표시 | ||
| if (routes.length === 0 && sharedBytes > 0) { | ||
| routes = [{ | ||
| path: '/', | ||
| size: '0 B', | ||
| firstLoad: formatBytes(sharedBytes), | ||
| firstLoadKb: toKb(sharedBytes), | ||
| }]; | ||
| } | ||
| } | ||
|
|
||
| return { routes, sharedSize: formatBytes(sharedBytes) }; | ||
| }; | ||
|
|
||
| const WARN_THRESHOLD_KB = 200; | ||
| const ERROR_THRESHOLD_KB = 350; | ||
|
|
||
| const sizeIcon = (firstLoadKb) => { | ||
| if (firstLoadKb >= ERROR_THRESHOLD_KB) return '🔴'; | ||
| if (firstLoadKb >= WARN_THRESHOLD_KB) return '🟡'; | ||
| return '🟢'; | ||
| }; | ||
|
|
||
| const formatRouteTable = (appLabel, { routes, sharedSize }) => { | ||
| if (!routes.length) { | ||
| return `### ${appLabel}\n\n> ⚠️ 번들 크기 데이터를 가져오지 못했습니다 (.next 디렉토리를 확인하세요).\n`; | ||
| } | ||
|
|
||
| const rows = routes | ||
| .map( | ||
| (r) => | ||
| `| \`${r.path}\` | ${r.size} | ${r.firstLoad} | ${sizeIcon(r.firstLoadKb)} |` | ||
| ) | ||
| .join('\n'); | ||
|
|
||
| const sharedLine = sharedSize ? `\n> 공유 번들: **${sharedSize}**\n` : ''; | ||
|
|
||
| return `### ${appLabel} | ||
|
|
||
| | 라우트 | 크기 | First Load JS | 상태 | | ||
| |--------|------|---------------|------| | ||
| ${rows} | ||
| ${sharedLine}`; | ||
| }; | ||
|
|
||
| module.exports = async ({ github, context, core }) => { | ||
| const prNumber = context.payload.pull_request?.number; | ||
| if (!prNumber) { | ||
| core.warning('PR 컨텍스트를 찾을 수 없습니다.'); | ||
| return; | ||
| } | ||
|
|
||
| const { owner, repo } = context.repo; | ||
|
|
||
| core.info(`번들 분석 경로: ${BUILD_DIR}`); | ||
| const timoWebBuild = analyzeBuild(BUILD_DIR); | ||
|
|
||
| if (!timoWebBuild.routes.length) { | ||
| core.warning(`번들 크기 데이터를 파싱하지 못했습니다. ${BUILD_DIR} 를 확인하세요.`); | ||
| return; | ||
| } | ||
|
|
||
| const legend = `> 🟢 정상 (<${WARN_THRESHOLD_KB}kB) 🟡 주의 (<${ERROR_THRESHOLD_KB}kB) 🔴 초과 (≥${ERROR_THRESHOLD_KB}kB) — First Load JS 기준 (gzip 크기)`; | ||
|
|
||
| const body = `## 📦 번들 사이즈 리포트 | ||
|
|
||
| ${formatRouteTable('🕐 Timo Web', timoWebBuild)} | ||
|
|
||
| --- | ||
|
|
||
| ${legend} | ||
|
|
||
| *빌드 커밋: \`${context.sha.slice(0, 7)}\`*`; | ||
|
|
||
| const { data: comments } = await github.rest.issues.listComments({ | ||
| owner, | ||
| repo, | ||
| issue_number: prNumber, | ||
| }); | ||
|
|
||
| const existing = comments.find( | ||
| (c) => c.user?.type === 'Bot' && c.body?.includes('번들 사이즈 리포트') | ||
| ); | ||
|
|
||
| if (existing) { | ||
| await github.rest.issues.updateComment({ | ||
| owner, | ||
| repo, | ||
| comment_id: existing.id, | ||
| body, | ||
| }); | ||
| } else { | ||
| await github.rest.issues.createComment({ | ||
| owner, | ||
| repo, | ||
| issue_number: prNumber, | ||
| body, | ||
| }); | ||
| } | ||
|
|
||
| core.info('번들 사이즈 리포트 PR 코멘트 게시 완료'); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| name: 📦 번들 사이즈 리포트 | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened] | ||
| branches: [main, develop] | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: write | ||
|
kimminna marked this conversation as resolved.
|
||
|
|
||
| concurrency: | ||
| group: bundle-size-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| bundle-size: | ||
| name: Bundle Size Report | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
kimminna marked this conversation as resolved.
|
||
|
|
||
| - uses: pnpm/action-setup@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 24 | ||
| cache: 'pnpm' | ||
|
|
||
| - name: Install dependencies | ||
| run: pnpm install --frozen-lockfile | ||
|
|
||
| - name: Build timo-web | ||
| id: build | ||
| env: | ||
| TURBO_UI: false | ||
| NO_COLOR: '1' | ||
| run: pnpm turbo run build --filter=timo-web --output-logs=full 2>&1 | tee /tmp/timo-web-build.txt | ||
| continue-on-error: true | ||
|
kimminna marked this conversation as resolved.
|
||
|
|
||
| - name: Debug - 빌드 결과 확인 | ||
| if: steps.build.outcome == 'failure' | ||
| run: | | ||
| echo "=== .next 디렉토리 ===" | ||
| ls apps/timo-web/.next/ 2>/dev/null || echo "(.next 없음)" | ||
| echo "=== routes-manifest staticRoutes ===" | ||
| node -e "const m=require('./apps/timo-web/.next/routes-manifest.json');console.log(JSON.stringify(m.staticRoutes,null,2))" 2>/dev/null || echo "(없음)" | ||
| echo "=== build-manifest rootMainFiles ===" | ||
| node -e "const m=require('./apps/timo-web/.next/build-manifest.json');console.log(JSON.stringify(m.rootMainFiles,null,2))" 2>/dev/null || echo "(없음)" | ||
| echo "=== static/chunks/app 디렉토리 ===" | ||
| ls apps/timo-web/.next/static/chunks/app/ 2>/dev/null || echo "(없음)" | ||
|
|
||
| - name: 번들 사이즈 PR 코멘트 게시 | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const fn = require('./.github/scripts/bundle-size-report.js') | ||
| await fn({ github, context, core }) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.