Skip to content

Commit 7dc85d2

Browse files
authored
Merge pull request #122 from InningLog/chore/#120/api-docs-pipeline
fix : 스냅샷 기반 diff로 신규/변경 엔드포인트만 문서 생성 #120
2 parents ab5dcfb + 0a60289 commit 7dc85d2

4 files changed

Lines changed: 129 additions & 22 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,21 @@ jobs:
107107
repository: InningLog/inninglog-api-docs
108108
token: ${{ secrets.DOCS_REPO_TOKEN }}
109109
path: docs-repo
110+
fetch-depth: 0
111+
112+
- name: Restore previous auto-generated files
113+
working-directory: docs-repo
114+
run: |
115+
BRANCH="auto/api-docs-update"
116+
if git rev-parse --verify "origin/$BRANCH" >/dev/null 2>&1; then
117+
git diff --name-only origin/main "origin/$BRANCH" | while IFS= read -r file; do
118+
mkdir -p "$(dirname "$file")"
119+
git show "origin/$BRANCH:$file" > "$file" 2>/dev/null || true
120+
done
121+
echo "Restored files from previous auto branch"
122+
else
123+
echo "No previous auto branch found"
124+
fi
110125
111126
- name: Run converter
112127
run: node scripts/openapi-to-gitbook/src/index.js -i openapi.json -o docs-repo

scripts/openapi-to-gitbook/src/converter.js

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,39 @@
22

33
const fs = require('fs');
44
const path = require('path');
5+
const crypto = require('crypto');
56
const { getDir } = require('./domainMapper');
6-
const { generateEndpointMarkdown, AUTO_MARKER } = require('./markdownGen');
7-
const { mergeSummary } = require('./summaryGen');
7+
const { generateEndpointMarkdown, AUTO_MARKER, resolveRef } = require('./markdownGen');
8+
const { scanAndMergeSummary } = require('./summaryGen');
9+
10+
const SNAPSHOT_FILE = '.openapi-snapshot.json';
11+
const METHODS = ['get', 'post', 'put', 'patch', 'delete'];
12+
13+
function collectRefs(obj, spec, depth, refs) {
14+
if (!obj || typeof obj !== 'object' || depth > 3) return refs;
15+
16+
if (obj.$ref && !refs[obj.$ref]) {
17+
const resolved = resolveRef(obj.$ref, spec);
18+
if (resolved) {
19+
refs[obj.$ref] = resolved;
20+
collectRefs(resolved, spec, depth + 1, refs);
21+
}
22+
}
23+
24+
if (Array.isArray(obj)) {
25+
for (const item of obj) collectRefs(item, spec, depth, refs);
26+
} else {
27+
for (const val of Object.values(obj)) collectRefs(val, spec, depth, refs);
28+
}
29+
30+
return refs;
31+
}
32+
33+
function computeHash(operation, spec) {
34+
const refs = collectRefs(operation, spec, 0, {});
35+
const input = JSON.stringify({ operation, refs });
36+
return crypto.createHash('md5').update(input).digest('hex');
37+
}
838

939
function generateFileName(method, pathStr) {
1040
const segments = pathStr
@@ -17,18 +47,51 @@ function generateFileName(method, pathStr) {
1747
}
1848

1949
function convert(spec, outputDir) {
20-
const endpoints = [];
50+
const snapshotPath = path.join(outputDir, SNAPSHOT_FILE);
51+
52+
// Load previous snapshot (endpoint hash map)
53+
let prevHashes = null;
54+
if (fs.existsSync(snapshotPath)) {
55+
try {
56+
prevHashes = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'));
57+
} catch {
58+
prevHashes = null;
59+
}
60+
}
61+
62+
// Build current hash map
63+
const currentHashes = {};
64+
for (const [pathStr, pathItem] of Object.entries(spec.paths || {})) {
65+
for (const method of METHODS) {
66+
if (!pathItem[method]) continue;
67+
const key = `${method.toUpperCase()} ${pathStr}`;
68+
currentHashes[key] = computeHash(pathItem[method], spec);
69+
}
70+
}
71+
72+
// First run: save baseline, generate nothing
73+
if (!prevHashes) {
74+
console.log('No previous snapshot found. Saving baseline snapshot.');
75+
fs.mkdirSync(outputDir, { recursive: true });
76+
fs.writeFileSync(snapshotPath, JSON.stringify(currentHashes, null, 2), 'utf-8');
77+
return { filesWritten: 0, skipped: 0, domains: 0 };
78+
}
79+
80+
// Find new/changed endpoints
2181
let filesWritten = 0;
2282
let skipped = 0;
2383
const domainSet = new Set();
2484

25-
const methods = ['get', 'post', 'put', 'patch', 'delete'];
26-
2785
for (const [pathStr, pathItem] of Object.entries(spec.paths || {})) {
28-
for (const method of methods) {
86+
for (const method of METHODS) {
2987
const operation = pathItem[method];
3088
if (!operation) continue;
3189

90+
const key = `${method.toUpperCase()} ${pathStr}`;
91+
92+
// Skip unchanged endpoints
93+
if (prevHashes[key] === currentHashes[key]) continue;
94+
3295
const tag = operation.tags?.[0] || '기타';
3396
const dir = getDir(tag);
3497
domainSet.add(dir);
@@ -50,17 +113,14 @@ function convert(spec, outputDir) {
50113
fs.mkdirSync(path.dirname(filePath), { recursive: true });
51114
fs.writeFileSync(filePath, markdown, 'utf-8');
52115
filesWritten++;
53-
54-
endpoints.push({
55-
tag,
56-
dir,
57-
fileName,
58-
title: operation.summary || `${method.toUpperCase()} ${pathStr}`,
59-
});
60116
}
61117
}
62118

63-
mergeSummary(outputDir, endpoints);
119+
// Save updated snapshot
120+
fs.writeFileSync(snapshotPath, JSON.stringify(currentHashes, null, 2), 'utf-8');
121+
122+
// Scan all auto-generated files and merge SUMMARY.md
123+
scanAndMergeSummary(outputDir);
64124

65125
return { filesWritten, skipped, domains: domainSet.size };
66126
}

scripts/openapi-to-gitbook/src/markdownGen.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,4 @@ function generateEndpointMarkdown(method, pathStr, operation, spec) {
263263
return lines.join('\n');
264264
}
265265

266-
module.exports = { generateEndpointMarkdown, AUTO_MARKER };
266+
module.exports = { generateEndpointMarkdown, AUTO_MARKER, resolveRef };

scripts/openapi-to-gitbook/src/summaryGen.js

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,47 @@
33
const fs = require('fs');
44
const path = require('path');
55
const { getTagLabel } = require('./domainMapper');
6+
const { AUTO_MARKER } = require('./markdownGen');
67

78
const START_MARKER = '<!-- AUTO-GENERATED-START -->';
89
const END_MARKER = '<!-- AUTO-GENERATED-END -->';
910

11+
function scanAutoGeneratedFiles(outputDir) {
12+
const endpoints = [];
13+
14+
let entries;
15+
try {
16+
entries = fs.readdirSync(outputDir, { withFileTypes: true });
17+
} catch {
18+
return endpoints;
19+
}
20+
21+
const dirs = entries.filter(d => d.isDirectory()).map(d => d.name);
22+
23+
for (const dir of dirs) {
24+
const dirPath = path.join(outputDir, dir);
25+
let files;
26+
try {
27+
files = fs.readdirSync(dirPath).filter(f => f.endsWith('.md'));
28+
} catch {
29+
continue;
30+
}
31+
32+
for (const file of files) {
33+
const content = fs.readFileSync(path.join(dirPath, file), 'utf-8');
34+
if (!content.startsWith(AUTO_MARKER)) continue;
35+
36+
const titleMatch = content.match(/^## (.+)$/m);
37+
const title = titleMatch ? titleMatch[1] : file.replace('.md', '');
38+
39+
endpoints.push({ dir, fileName: file, title, tag: getTagLabel(dir) });
40+
}
41+
}
42+
43+
return endpoints;
44+
}
45+
1046
function buildAutoSection(endpoints) {
11-
// Group by domain directory
1247
const groups = {};
1348
for (const ep of endpoints) {
1449
if (!groups[ep.dir]) {
@@ -19,7 +54,6 @@ function buildAutoSection(endpoints) {
1954

2055
const lines = [START_MARKER, ''];
2156

22-
// Sort domains alphabetically for consistency
2357
const sortedDirs = Object.keys(groups).sort();
2458
for (const dir of sortedDirs) {
2559
const group = groups[dir];
@@ -34,14 +68,14 @@ function buildAutoSection(endpoints) {
3468
return lines.join('\n');
3569
}
3670

37-
function mergeSummary(outputDir, endpoints) {
71+
function scanAndMergeSummary(outputDir) {
72+
const endpoints = scanAutoGeneratedFiles(outputDir);
3873
if (endpoints.length === 0) return;
3974

4075
const summaryPath = path.join(outputDir, 'SUMMARY.md');
4176
const autoSection = buildAutoSection(endpoints);
4277

4378
if (!fs.existsSync(summaryPath)) {
44-
// No existing SUMMARY.md — create one with auto section
4579
const content = `# Table of contents\n\n* [소개](README.md)\n\n${autoSection}\n`;
4680
fs.writeFileSync(summaryPath, content, 'utf-8');
4781
return;
@@ -53,16 +87,14 @@ function mergeSummary(outputDir, endpoints) {
5387

5488
let merged;
5589
if (startIdx !== -1 && endIdx !== -1) {
56-
// Replace existing auto section
5790
const before = existing.substring(0, startIdx);
5891
const after = existing.substring(endIdx + END_MARKER.length);
5992
merged = before + autoSection + after;
6093
} else {
61-
// Append auto section at the end
6294
merged = existing.trimEnd() + '\n\n' + autoSection + '\n';
6395
}
6496

6597
fs.writeFileSync(summaryPath, merged, 'utf-8');
6698
}
6799

68-
module.exports = { mergeSummary };
100+
module.exports = { scanAndMergeSummary };

0 commit comments

Comments
 (0)