Skip to content

Commit e4f878c

Browse files
committed
Add engineering documentation metadata QA gate
1 parent 6e5a761 commit e4f878c

7 files changed

Lines changed: 238 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,18 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- uses: actions/checkout@v6
16-
- uses: actions/setup-node@v4
16+
- uses: actions/setup-node@v6
1717
with:
1818
node-version: '20'
1919
cache: npm
2020
- name: Install
2121
run: npm ci
2222
- name: Test
2323
run: npm test
24+
- name: Setup Ruby
25+
uses: ruby/setup-ruby@v1
26+
with:
27+
ruby-version: '3.3'
28+
bundler-cache: true
29+
- name: Build Jekyll site
30+
run: npm run build

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,17 @@ npm start
3030
# ビルド
3131
npm run build
3232

33-
# テスト(markdown lint / link check)
33+
# メタデータ整合性チェック
34+
npm run check:metadata
35+
36+
# テスト(metadata / markdown lint / link check)
3437
npm test
3538
```
3639

40+
### 品質ゲート
41+
42+
`npm run check:metadata` は、`book-config.json` / `package.json` / `package-lock.json` / Jekyll 設定 / `docs/index.md` / ナビゲーションのタイトル・説明・版数・公開 URL がずれていないことを検証します。
43+
3744
## ライセンス
3845

3946
本書は **CC BY-NC-SA 4.0** で提供します。詳細は `LICENSE.md` を参照してください。

_config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
title: エンジニアリングドキュメント実践ガイド:README・手順書・Runbook・ADR・ポストモーテム
22
description: 実務で使える技術ドキュメント成果物(README/手順書/Runbook/ADR/障害報告・ポストモーテム等)の設計と運用を扱う。
33
author: 株式会社アイティードゥ
4+
version: "0.1.0"
45
lang: ja
56
url: https://itdojp.github.io
67
baseurl: /engineering-documentation-book

docs/_config.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
title: エンジニアリングドキュメント実践ガイド:README・手順書・Runbook・ADR・ポストモーテム
2-
description: 実務で使える技術ドキュメント成果物(README/手順書/Runbook/ADR/障害報告・ポストモーテムなど)の設計と運用を扱う。
2+
description: 実務で使える技術ドキュメント成果物(README/手順書/Runbook/ADR/障害報告・ポストモーテム等)の設計と運用を扱う。
33
author: 株式会社アイティードゥ
4+
version: "0.1.0"
45
lang: ja
56
url: https://itdojp.github.io
67
baseurl: /engineering-documentation-book

docs/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
---
22
title: "エンジニアリングドキュメント実践ガイド:README・手順書・Runbook・ADR・ポストモーテム"
3-
description: "未経験〜初級が、実務で求められるドキュメント成果物(README/手順書/Runbook/ADR/障害報告・ポストモーテム)を作成できる状態にする。"
3+
description: "実務で使える技術ドキュメント成果物(README/手順書/Runbook/ADR/障害報告・ポストモーテム等)の設計と運用を扱う。"
4+
author: "株式会社アイティードゥ"
5+
version: "0.1.0"
46
layout: book
57
order: 0
68
readerProfile:

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@
66
"scripts": {
77
"start": "bash scripts/jekyll.sh serve",
88
"build": "bash scripts/jekyll.sh build",
9-
"test": "npm run lint && npm run check-links",
9+
"test": "npm run check:metadata && npm run lint && npm run check-links",
1010
"lint": "markdownlint -c .markdownlint.json \"docs/**/*.md\"",
1111
"check-links": "find docs -name '*.md' -print0 | xargs -0 -n 1 markdown-link-check -c .markdown-link-check.json",
12-
"deploy": "gh-pages -d _site"
12+
"deploy": "gh-pages -d _site",
13+
"check:metadata": "node scripts/check-metadata-consistency.js"
1314
},
1415
"license": "CC-BY-NC-SA-4.0",
1516
"devDependencies": {
@@ -20,5 +21,9 @@
2021
"repository": {
2122
"type": "git",
2223
"url": "https://github.com/itdojp/engineering-documentation-book.git"
24+
},
25+
"homepage": "https://itdojp.github.io/engineering-documentation-book/",
26+
"bugs": {
27+
"url": "https://github.com/itdojp/engineering-documentation-book/issues"
2328
}
2429
}
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
const fs = require('fs');
5+
const path = require('path');
6+
7+
const root = process.cwd();
8+
const errors = [];
9+
10+
function readText(file) {
11+
return fs.readFileSync(path.join(root, file), 'utf8');
12+
}
13+
14+
function readJson(file) {
15+
return JSON.parse(readText(file));
16+
}
17+
18+
function unquote(value) {
19+
const trimmed = String(value).trim();
20+
if (
21+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
22+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
23+
) {
24+
return trimmed.slice(1, -1);
25+
}
26+
return trimmed;
27+
}
28+
29+
function parseYamlScalars(text) {
30+
const result = {};
31+
for (const line of text.split(/\r?\n/)) {
32+
if (!line.trim() || line.trimStart().startsWith('#')) {
33+
continue;
34+
}
35+
const match = line.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/);
36+
if (!match) {
37+
continue;
38+
}
39+
result[match[1]] = unquote(match[2]);
40+
}
41+
return result;
42+
}
43+
44+
function parseFrontMatter(file) {
45+
const text = readText(file);
46+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
47+
if (!match) {
48+
errors.push(`${file}: front matter is missing`);
49+
return {};
50+
}
51+
return parseYamlScalars(match[1]);
52+
}
53+
54+
function parseNavigationList(text, section) {
55+
const items = [];
56+
let inSection = false;
57+
let current = null;
58+
59+
for (const line of text.split(/\r?\n/)) {
60+
const sectionMatch = line.match(/^([A-Za-z0-9_-]+):\s*$/);
61+
if (sectionMatch) {
62+
if (current) {
63+
items.push(current);
64+
current = null;
65+
}
66+
inSection = sectionMatch[1] === section;
67+
continue;
68+
}
69+
70+
if (!inSection) {
71+
continue;
72+
}
73+
74+
if (!line.trim()) {
75+
continue;
76+
}
77+
78+
const titleMatch = line.match(/^\s*-\s+title:\s*(.+)$/);
79+
if (titleMatch) {
80+
if (current) {
81+
items.push(current);
82+
}
83+
current = { title: unquote(titleMatch[1]) };
84+
continue;
85+
}
86+
87+
const pathMatch = line.match(/^\s+path:\s*(.+)$/);
88+
if (pathMatch && current) {
89+
current.path = unquote(pathMatch[1]);
90+
}
91+
}
92+
93+
if (current) {
94+
items.push(current);
95+
}
96+
97+
return items;
98+
}
99+
100+
function expectEqual(label, actual, expected) {
101+
if (actual !== expected) {
102+
errors.push(`${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
103+
}
104+
}
105+
106+
function expectTruthy(label, actual) {
107+
if (!actual) {
108+
errors.push(`${label}: expected a non-empty value`);
109+
}
110+
}
111+
112+
function expectFile(file) {
113+
if (!fs.existsSync(path.join(root, file))) {
114+
errors.push(`${file}: expected file to exist`);
115+
return false;
116+
}
117+
return true;
118+
}
119+
120+
const bookConfig = readJson('book-config.json');
121+
const pkg = readJson('package.json');
122+
const lock = readJson('package-lock.json');
123+
const rootConfig = parseYamlScalars(readText('_config.yml'));
124+
const docsConfig = parseYamlScalars(readText('docs/_config.yml'));
125+
const docsIndex = parseFrontMatter('docs/index.md');
126+
const navigationText = readText('docs/_data/navigation.yml');
127+
128+
const repositoryUrl = new URL(pkg.repository.url.replace(/\.git$/, ''));
129+
const [owner, repo] = repositoryUrl.pathname.replace(/^\//, '').split('/');
130+
const pagesUrl = `https://${owner}.github.io/${repo}/`;
131+
const repoUrl = `https://github.com/${owner}/${repo}`;
132+
const issuesUrl = `${repoUrl}/issues`;
133+
const baseurl = `/${repo}`;
134+
135+
expectEqual('package.json name', pkg.name, repo);
136+
expectEqual('package.json version', pkg.version, bookConfig.version);
137+
expectEqual('package.json description', pkg.description, bookConfig.description);
138+
expectEqual('package.json license', pkg.license, 'CC-BY-NC-SA-4.0');
139+
expectEqual('package.json repository.url', pkg.repository && pkg.repository.url, `${repoUrl}.git`);
140+
expectEqual('package.json homepage', pkg.homepage, pagesUrl);
141+
expectEqual('package.json bugs.url', pkg.bugs && pkg.bugs.url, issuesUrl);
142+
expectEqual('package.json scripts.check:metadata', pkg.scripts && pkg.scripts['check:metadata'], 'node scripts/check-metadata-consistency.js');
143+
144+
expectEqual('package-lock.json name', lock.name, repo);
145+
expectEqual('package-lock.json version', lock.version, bookConfig.version);
146+
expectEqual('package-lock.json packages[""].name', lock.packages && lock.packages[''] && lock.packages[''].name, repo);
147+
expectEqual('package-lock.json packages[""].version', lock.packages && lock.packages[''] && lock.packages[''].version, bookConfig.version);
148+
expectEqual('package-lock.json packages[""].license', lock.packages && lock.packages[''] && lock.packages[''].license, 'CC-BY-NC-SA-4.0');
149+
150+
for (const [file, config] of [['_config.yml', rootConfig], ['docs/_config.yml', docsConfig]]) {
151+
expectEqual(`${file} title`, config.title, bookConfig.title);
152+
expectEqual(`${file} description`, config.description, bookConfig.description);
153+
expectEqual(`${file} author`, config.author, bookConfig.author);
154+
expectEqual(`${file} version`, config.version, bookConfig.version);
155+
expectEqual(`${file} url`, config.url, `https://${owner}.github.io`);
156+
expectEqual(`${file} baseurl`, config.baseurl, baseurl);
157+
expectEqual(`${file} repository`, config.repository, repoUrl);
158+
}
159+
160+
expectEqual('docs/index.md front matter title', docsIndex.title, bookConfig.title);
161+
expectEqual('docs/index.md front matter description', docsIndex.description, bookConfig.description);
162+
expectEqual('docs/index.md front matter author', docsIndex.author, bookConfig.author);
163+
expectEqual('docs/index.md front matter version', docsIndex.version, bookConfig.version);
164+
165+
const navigationChapters = parseNavigationList(navigationText, 'chapters');
166+
const navigationAppendices = parseNavigationList(navigationText, 'appendices');
167+
expectEqual('navigation chapter count', navigationChapters.length, bookConfig.structure.chapters.length);
168+
expectEqual('navigation appendix count', navigationAppendices.length, bookConfig.structure.appendices.length);
169+
170+
bookConfig.structure.chapters.forEach((chapter, index) => {
171+
const expectedPath = `/chapters/${chapter.id}/`;
172+
const item = navigationChapters[index] || {};
173+
expectEqual(`navigation chapter[${index}].title`, item.title, chapter.title);
174+
expectEqual(`navigation chapter[${index}].path`, item.path, expectedPath);
175+
176+
const file = `docs/chapters/${chapter.id}/index.md`;
177+
if (expectFile(file)) {
178+
const front = parseFrontMatter(file);
179+
expectEqual(`${file} front matter title`, front.title, chapter.title);
180+
expectTruthy(`${file} front matter order`, front.order);
181+
}
182+
});
183+
184+
bookConfig.structure.appendices.forEach((appendix, index) => {
185+
const expectedPath = `/appendices/${appendix.id}/`;
186+
const item = navigationAppendices[index] || {};
187+
expectEqual(`navigation appendix[${index}].title`, item.title, appendix.title);
188+
expectEqual(`navigation appendix[${index}].path`, item.path, expectedPath);
189+
190+
const file = `docs/appendices/${appendix.id}/index.md`;
191+
if (expectFile(file)) {
192+
const front = parseFrontMatter(file);
193+
expectEqual(`${file} front matter title`, front.title, appendix.title);
194+
expectTruthy(`${file} front matter order`, front.order);
195+
}
196+
});
197+
198+
if (errors.length) {
199+
console.error('Metadata consistency check failed:');
200+
for (const error of errors) {
201+
console.error(`- ${error}`);
202+
}
203+
process.exit(1);
204+
}
205+
206+
console.log('Metadata consistency check passed.');
207+
console.log(`Repository: ${owner}/${repo}`);
208+
console.log(`Version: ${bookConfig.version}`);
209+
console.log(`Pages: ${pagesUrl}`);

0 commit comments

Comments
 (0)