Skip to content

Commit 921a237

Browse files
Add validation quality checks
1 parent 5d1d14c commit 921a237

5 files changed

Lines changed: 260 additions & 1 deletion

File tree

.github/workflows/validate.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Validate Docs
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
validate:
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Check out repository
13+
uses: actions/checkout@v4
14+
15+
- name: Set up Node.js
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: "20"
19+
20+
- name: Run validation
21+
run: node scripts/validate.js
22+

CONTRIBUTING.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,13 @@ Thanks for helping improve GitHub Docs Templates.
2424
2. Create a branch.
2525
3. Make your change.
2626
4. Check that links work.
27-
5. Open a pull request with a short explanation.
27+
5. Run `node scripts/validate.js` if you can.
28+
6. Open a pull request with a short explanation.
2829

30+
## Quality Check
31+
32+
Before opening a pull request, this command can catch common mistakes:
33+
34+
```bash
35+
node scripts/validate.js
36+
```

QUALITY.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Quality Checks
2+
3+
This repository includes a small validation script to catch common documentation mistakes.
4+
5+
## Run Checks
6+
7+
```bash
8+
node scripts/validate.js
9+
```
10+
11+
## What It Checks
12+
13+
- Markdown relative links point to real files.
14+
- Required root files are present.
15+
- Every starter pack has its expected files.
16+
- Every template category has its own `README.md` guide.
17+
- A few common typo or naming mistakes are not present.
18+
19+
## GitHub Actions
20+
21+
The same validation runs automatically on pushes and pull requests through:
22+
23+
```text
24+
.github/workflows/validate.yml
25+
```

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ If you want to choose individual files instead, use `templates/`.
3030

3131
For a compact list of the most useful templates, see [TEMPLATE_INDEX.md](TEMPLATE_INDEX.md).
3232

33+
## Quality Checks
34+
35+
This repository includes a small validation script:
36+
37+
```bash
38+
node scripts/validate.js
39+
```
40+
41+
It checks relative links, starter packs, and template folder guides. See [QUALITY.md](QUALITY.md).
42+
3343
## Do I Need All Of This?
3444

3545
No. This repository includes many docs because different projects need different things.
@@ -87,6 +97,7 @@ When you use a template in your own repository, rename it to the standard GitHub
8797
- `examples/` - complete example documentation sets for common project types.
8898
- `guides/` - beginner explanations, checklists, and glossary.
8999
- `TEMPLATE_INDEX.md` - compact index of common copy paths.
100+
- `QUALITY.md` - validation notes for maintaining the repository.
90101

91102
## Placeholder Style
92103

scripts/validate.js

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
const root = process.cwd();
5+
const starterPacks = [
6+
"starter-packs/personal-project",
7+
"starter-packs/open-source-library",
8+
"starter-packs/web-app",
9+
"starter-packs/cli-tool",
10+
"starter-packs/docs-site",
11+
];
12+
13+
const expectedStarterFiles = {
14+
"starter-packs/personal-project": [
15+
"README.md",
16+
"LICENSE",
17+
"CONTRIBUTING.md",
18+
],
19+
"starter-packs/open-source-library": [
20+
"README.md",
21+
"LICENSE",
22+
"CONTRIBUTING.md",
23+
"CODE_OF_CONDUCT.md",
24+
"SECURITY.md",
25+
".github/ISSUE_TEMPLATE/bug_report.md",
26+
".github/ISSUE_TEMPLATE/feature_request.md",
27+
".github/PULL_REQUEST_TEMPLATE.md",
28+
],
29+
"starter-packs/web-app": [
30+
"README.md",
31+
"SECURITY.md",
32+
"SUPPORT.md",
33+
".github/ISSUE_TEMPLATE/bug_report.md",
34+
],
35+
"starter-packs/cli-tool": [
36+
"README.md",
37+
"CONTRIBUTING.md",
38+
"CHANGELOG.md",
39+
],
40+
"starter-packs/docs-site": [
41+
"README.md",
42+
"CONTRIBUTING.md",
43+
".github/ISSUE_TEMPLATE/documentation.md",
44+
],
45+
};
46+
47+
function walk(dir) {
48+
const files = [];
49+
50+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
51+
if (entry.name === ".git") continue;
52+
53+
const fullPath = path.join(dir, entry.name);
54+
if (entry.isDirectory()) {
55+
files.push(...walk(fullPath));
56+
} else {
57+
files.push(fullPath);
58+
}
59+
}
60+
61+
return files;
62+
}
63+
64+
function fail(message) {
65+
console.error(message);
66+
process.exitCode = 1;
67+
}
68+
69+
function checkMarkdownLinks() {
70+
const missing = [];
71+
const files = walk(root).filter((file) => file.endsWith(".md"));
72+
const linkPattern = /\[[^\]]+\]\((?!https?:|mailto:|#)([^)]+)\)/g;
73+
74+
for (const file of files) {
75+
const text = fs.readFileSync(file, "utf8");
76+
let match;
77+
78+
while ((match = linkPattern.exec(text))) {
79+
const rawTarget = match[1].split("#")[0];
80+
if (!rawTarget) continue;
81+
82+
const target = decodeURIComponent(rawTarget);
83+
const fullTarget = path.normalize(path.join(path.dirname(file), target));
84+
85+
if (!fs.existsSync(fullTarget)) {
86+
missing.push(`${path.relative(root, file)} -> ${match[1]}`);
87+
}
88+
}
89+
}
90+
91+
if (missing.length) {
92+
fail(`Missing markdown links:\n${missing.join("\n")}`);
93+
} else {
94+
console.log("Markdown relative links OK");
95+
}
96+
}
97+
98+
function checkStarterPacks() {
99+
for (const pack of starterPacks) {
100+
const fullPath = path.join(root, pack);
101+
102+
if (!fs.existsSync(fullPath)) {
103+
fail(`Missing starter pack: ${pack}`);
104+
continue;
105+
}
106+
107+
for (const file of expectedStarterFiles[pack]) {
108+
const filePath = path.join(fullPath, file);
109+
110+
if (!fs.existsSync(filePath)) {
111+
fail(`Starter pack is missing ${file}: ${pack}`);
112+
}
113+
}
114+
115+
console.log(`Starter pack OK: ${pack}`);
116+
}
117+
}
118+
119+
function checkRootFiles() {
120+
const required = [
121+
"README.md",
122+
"QUICKSTART.md",
123+
"TEMPLATE_INDEX.md",
124+
"LICENSE",
125+
"CONTRIBUTING.md",
126+
"CODE_OF_CONDUCT.md",
127+
"SECURITY.md",
128+
];
129+
130+
for (const file of required) {
131+
if (!fs.existsSync(path.join(root, file))) {
132+
fail(`Missing root file: ${file}`);
133+
}
134+
}
135+
136+
console.log("Root files OK");
137+
}
138+
139+
function checkTemplateReadmes() {
140+
const templatesDir = path.join(root, "templates");
141+
const missing = [];
142+
143+
for (const entry of fs.readdirSync(templatesDir, { withFileTypes: true })) {
144+
if (!entry.isDirectory()) continue;
145+
146+
const readmePath = path.join(templatesDir, entry.name, "README.md");
147+
if (!fs.existsSync(readmePath)) {
148+
missing.push(`templates/${entry.name}/README.md`);
149+
}
150+
}
151+
152+
if (missing.length) {
153+
fail(`Missing template folder guides:\n${missing.join("\n")}`);
154+
} else {
155+
console.log("Template folder guides OK");
156+
}
157+
}
158+
159+
function checkUnwantedText() {
160+
const unwanted = ["Placholder", "github-docs-templates"];
161+
const matches = [];
162+
163+
for (const file of walk(root)) {
164+
if (file.includes(`${path.sep}.git${path.sep}`)) continue;
165+
if (path.relative(root, file) === "scripts/validate.js") continue;
166+
167+
const text = fs.readFileSync(file, "utf8");
168+
169+
for (const word of unwanted) {
170+
if (text.includes(word)) {
171+
matches.push(`${path.relative(root, file)} contains ${word}`);
172+
}
173+
}
174+
}
175+
176+
if (matches.length) {
177+
fail(`Unwanted text found:\n${matches.join("\n")}`);
178+
} else {
179+
console.log("Unwanted text check OK");
180+
}
181+
}
182+
183+
checkMarkdownLinks();
184+
checkRootFiles();
185+
checkStarterPacks();
186+
checkTemplateReadmes();
187+
checkUnwantedText();
188+
189+
if (process.exitCode) {
190+
process.exit(process.exitCode);
191+
}
192+
193+
console.log("All validation checks passed");

0 commit comments

Comments
 (0)