Skip to content

Commit 3c68761

Browse files
fix: improve lint-readme.js error handling and messages (#130)
* ci: update linting error message to distinguish between files and directories * fix: add existence checks for files tracked in Git before validation * fix: improve file/directory type detection in README.md validation * fix: refactor item type detection for README.md validation and improve naming checks * fix: unify variable naming for file and directory checks in README.md validation * fix: clarify comment for filtering files in README.md validation * fix: enhance README.md validation with improved item path logging and additional comments
1 parent 9079c41 commit 3c68761

File tree

1 file changed

+60
-26
lines changed

1 file changed

+60
-26
lines changed

.github/scripts/lint-readme.js

Lines changed: 60 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ try {
3737
// Split the output into an array of file paths
3838
const files = gitFiles.split('\n');
3939

40-
// Filter out .sh files in the root directory (excluding those in subdirectories)
40+
// Filter files in the root directory (excluding those in subdirectories)
4141
const fileExtensions = ['.sh', '.ps1', '.js', '.mjs', '.py'];
4242

4343
const filteredFiles = files.filter(file => {
@@ -65,46 +65,76 @@ function logIssue(message) {
6565
console.log(`${issueCount}. ${message}`);
6666
}
6767

68-
// Check if each .sh file is mentioned in the README.md
69-
allScripts.forEach(file => {
70-
if (!readme.includes(`${headingLevel} ${file}`)) {
71-
logIssue(`📝 The file ${file} is not mentioned in the README.md`);
68+
/**
69+
* Determines if a path is a file, directory, or doesn't exist
70+
* @param {string} itemPath - Path to check
71+
* @returns {string|null} - 'file', 'directory', or null if doesn't exist
72+
*/
73+
function getItemType(itemPath) {
74+
if (!fs.existsSync(itemPath)) {
75+
return null; // doesn't exist
76+
}
77+
return fs.statSync(itemPath).isDirectory() ? 'directory' : 'file';
78+
}
79+
80+
// Check if each file/directory is mentioned in the README.md
81+
allScripts.forEach(item => {
82+
if (!readme.includes(`${headingLevel} ${item}`)) {
83+
const itemPath = path.join(directoryPath, item);
84+
const type = getItemType(itemPath) || 'file/directory';
85+
logIssue(`📝 The ${type} ${item} is not mentioned in the README.md`);
7286
}
7387
});
7488

7589
// Check that all files follow the kebab-case naming convention
76-
allScripts.forEach(file => {
77-
if (!/^([a-z0-9]+-)*[a-z0-9]+(\.[a-z0-9]+)*$/.test(file)) {
78-
logIssue(`🔤 The file ${file} does not follow the kebab-case naming convention`);
90+
allScripts.forEach(item => {
91+
if (!/^([a-z0-9]+-)*[a-z0-9]+(\.[a-z0-9]+)*$/.test(item)) {
92+
const itemPath = path.join(directoryPath, item);
93+
const type = getItemType(itemPath) || 'file';
94+
logIssue(`🔤 The ${type} ${item} does not follow the kebab-case naming convention`);
7995
}
8096
});
8197

8298
// Check that all .sh files have execution permissions
83-
allScripts.forEach(file => {
84-
if (!file.endsWith('.sh')) {
99+
allScripts.forEach(item => {
100+
if (!item.endsWith('.sh')) {
85101
return;
86102
}
87103

88-
const filePath = path.join(directoryPath, file);
89-
const stats = fs.statSync(filePath);
104+
const itemPath = path.join(directoryPath, item);
105+
106+
// Check if file exists before trying to stat it
107+
if (!fs.existsSync(itemPath)) {
108+
logIssue(`⚠️ The file ${item} is tracked in Git but does not exist on the filesystem`);
109+
return;
110+
}
111+
112+
const stats = fs.statSync(itemPath);
90113
const isExecutable = (stats.mode & fs.constants.X_OK) !== 0;
91114

92115
if (!isExecutable) {
93-
logIssue(`🔒 The file ${file} does not have execution permissions`);
116+
logIssue(`🔒 The file ${item} does not have execution permissions`);
94117
}
95118
});
96119

97120
// Check bash syntax for all .sh files
98-
allScripts.forEach(file => {
99-
if (!file.endsWith('.sh')) {
121+
allScripts.forEach(item => {
122+
if (!item.endsWith('.sh')) {
100123
return;
101124
}
102125

103-
const filePath = path.join(directoryPath, file);
126+
const itemPath = path.join(directoryPath, item);
127+
128+
// Check if file exists before trying to validate syntax
129+
if (!fs.existsSync(itemPath)) {
130+
// Already reported in the execution permissions check
131+
return;
132+
}
133+
104134
try {
105-
execSync(`bash -n "${filePath}"`, { stdio: 'pipe' });
135+
execSync(`bash -n "${itemPath}"`, { stdio: 'pipe' });
106136
} catch (error) {
107-
logIssue(`🐛 The file ${file} has a bash syntax error`);
137+
logIssue(`🐛 The file ${item} has a bash syntax error`);
108138
const errorLines = error.stderr.toString().trim().split('\n');
109139
errorLines.forEach(line => console.log(` ${line}`));
110140
}
@@ -126,32 +156,36 @@ if (!headings || headings.length === 0) {
126156
process.exit(1);
127157
}
128158

129-
// Check that all scripts mentioned in the README.md actually exist in the repository
159+
// Check that all items mentioned in the README.md actually exist in the repository
130160
headings.forEach(heading => {
131-
const script = heading.slice(headingLevel.length + 1); // Remove the '### ' prefix
132-
if (!allScripts.includes(script)) {
133-
logIssue(`📁 The script ${script} is mentioned in the README.md but does not exist in the repository`);
161+
const item = heading.slice(headingLevel.length + 1); // Remove the '### ' prefix
162+
if (!allScripts.includes(item)) {
163+
logIssue(`📁 The item "${item}" is mentioned in the README.md but does not exist at "${path.join(directoryPath, item)}"`);
134164
}
135165
});
136166

137-
// Check that certain short words are not used in the .sh file names
167+
// Check that certain short words are not used in file/directory names
138168
const shortWords = {
139169
'repo': 'repository',
140170
'repos': 'repositories',
141171
'org': 'organization',
142172
'orgs': 'organizations'
143173
};
144174

145-
allScripts.forEach(file => {
175+
allScripts.forEach(item => {
146176
Object.keys(shortWords).forEach(word => {
147177
const regex = new RegExp(`\\b${word}\\b`, 'g');
148-
if (regex.test(file)) {
149-
logIssue(`📏 The file name "${file}" uses the short word "${word}". Consider using "${shortWords[word]}" instead.`);
178+
if (regex.test(item)) {
179+
const itemPath = path.join(directoryPath, item);
180+
const type = getItemType(itemPath) || 'file';
181+
logIssue(`📏 The ${type} name "${item}" uses the short word "${word}". Consider using "${shortWords[word]}" instead.`);
150182
}
151183
});
152184
});
153185

154186
// Check if the headings are in alphabetical order
187+
// Special handling: prefixed items (e.g., 'add-user') should come after their prefix base (e.g., 'add')
188+
// but 'add-team-to-repository' should come before 'add-user' (standard alphabetical)
155189
for (let i = 0; i < headings.length - 1; i++) {
156190
const current = headings[i].toLowerCase();
157191
const next = headings[i + 1].toLowerCase();

0 commit comments

Comments
 (0)