Skip to content

Commit fc9df05

Browse files
committed
chore: update validation script output
1 parent 830d33f commit fc9df05

File tree

3 files changed

+189
-49
lines changed

3 files changed

+189
-49
lines changed

.github/workflows/validate-plugin-toml.yml

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ on:
77
paths:
88
- 'plugins/**'
99

10+
permissions:
11+
pull-requests: write
12+
1013
jobs:
1114
validate:
1215
runs-on: ubuntu-latest
@@ -42,5 +45,57 @@ jobs:
4245
run: pnpm install
4346

4447
- name: Validate plugin.toml files
48+
id: validate
4549
if: steps.changed.outputs.plugins != ''
46-
run: pnpm validate ${{ steps.changed.outputs.plugins }}
50+
continue-on-error: true
51+
env:
52+
VALIDATION_REPORT_PATH: validation-report.md
53+
run: |
54+
pnpm validate ${{ steps.changed.outputs.plugins }}
55+
echo "result=success" >> $GITHUB_OUTPUT
56+
# If validation fails (exit 1), continue-on-error keeps the workflow running
57+
# but 'result=success' won't be written to GITHUB_OUTPUT
58+
59+
- name: Post validation comment
60+
if: always() && steps.changed.outputs.plugins != ''
61+
uses: actions/github-script@v7
62+
with:
63+
script: |
64+
const fs = require('fs');
65+
const marker = '<!-- plugin-toml-validation -->';
66+
67+
let body;
68+
const reportPath = 'validation-report.md';
69+
if (fs.existsSync(reportPath)) {
70+
body = marker + '\n' + fs.readFileSync(reportPath, 'utf8');
71+
} else {
72+
body = marker + '\n## Plugin TOML Validation Report\n\n> No validation report generated.';
73+
}
74+
75+
const { data: comments } = await github.rest.issues.listComments({
76+
owner: context.repo.owner,
77+
repo: context.repo.repo,
78+
issue_number: context.issue.number,
79+
});
80+
81+
const existing = comments.find(c => c.body && c.body.includes(marker));
82+
83+
if (existing) {
84+
await github.rest.issues.updateComment({
85+
owner: context.repo.owner,
86+
repo: context.repo.repo,
87+
comment_id: existing.id,
88+
body,
89+
});
90+
} else {
91+
await github.rest.issues.createComment({
92+
owner: context.repo.owner,
93+
repo: context.repo.repo,
94+
issue_number: context.issue.number,
95+
body,
96+
});
97+
}
98+
99+
- name: Fail if validation failed
100+
if: steps.validate.outcome == 'failure'
101+
run: exit 1

generate.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,38 +109,40 @@ function generatePluginData(pluginsDir: string, gitmodulesPath: string): PluginD
109109
const pluginConfig = loadPluginToml(pluginPath)
110110

111111
if (!pluginConfig?.plugin) {
112-
console.warn(`警告: ${ pluginName } 没有有效的 plugin.toml`)
112+
console.warn(`Warning: ${ pluginName } has no valid plugin.toml`)
113113
continue
114114
}
115115

116116
const modulePath = `plugins/${ pluginName }`
117117
const gitModule = gitModules.get(modulePath)
118118

119119
if (!gitModule) {
120-
console.warn(`警告: ${ pluginName } 没有对应的 git submodule`)
120+
console.warn(`Warning: ${ pluginName } has no matching git submodule`)
121121
continue
122122
}
123123

124124
const rawPlugin = pluginConfig.plugin
125125

126-
// 验证并过滤 tags
126+
// Validate and filter tags
127127
let tags: ValidTag[] | undefined
128128
if (rawPlugin.tags && Array.isArray(rawPlugin.tags)) {
129129
const filtered = rawPlugin.tags
130130
.map(tag => tag.toLowerCase())
131131
.filter((tag): tag is ValidTag => VALID_TAGS.includes(tag as ValidTag))
132132
if (filtered.length > 0) {
133+
// @ts-ignore
133134
tags = [...new Set(filtered)]
134135
}
135136
}
136137

137-
// 验证并过滤 database
138+
// Validate and filter database
138139
let database: ValidDatabase[] | undefined
139140
if (rawPlugin.database && Array.isArray(rawPlugin.database)) {
140141
const filtered = rawPlugin.database
141142
.map(db => db.toLowerCase() as ValidDatabase)
142143
.filter((db): db is ValidDatabase => VALID_DATABASES.includes(db))
143144
if (filtered.length > 0) {
145+
// @ts-ignore
144146
database = [...new Set(filtered)]
145147
}
146148
}
@@ -201,10 +203,10 @@ function main() {
201203
const pluginsDir = path.join(baseDir, 'plugins')
202204
const gitmodulesPath = path.join(baseDir, '.gitmodules')
203205

204-
console.log('生成插件数据...')
206+
console.log('Generating plugins data...')
205207

206208
const pluginDataList = generatePluginData(pluginsDir, gitmodulesPath)
207-
console.log(`找到 ${ pluginDataList.length } 个插件`)
209+
console.log(`Found ${ pluginDataList.length } plugins`)
208210

209211
fs.writeFileSync(
210212
path.join(baseDir, 'plugins-data.ts'),
@@ -218,7 +220,7 @@ function main() {
218220
'utf-8'
219221
)
220222

221-
console.log('完成')
223+
console.log('Done')
222224
}
223225

224226
main()

validate.ts

Lines changed: 124 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -22,126 +22,209 @@ interface PluginToml {
2222
}
2323

2424
interface ValidationError {
25-
plugin: string
2625
field: string
2726
message: string
2827
invalidValues?: string[]
2928
}
3029

31-
function validatePluginToml(pluginName: string, pluginPath: string): ValidationError[] {
30+
interface ValidationResult {
31+
plugin: string
32+
passed: boolean
33+
config: PluginTomlPlugin | null
34+
errors: ValidationError[]
35+
}
36+
37+
const REQUIRED_FIELDS: (keyof Pick<PluginTomlPlugin, 'summary' | 'version' | 'description' | 'author'>)[] = [
38+
'summary',
39+
'version',
40+
'description',
41+
'author',
42+
]
43+
44+
const ALL_FIELDS: (keyof PluginTomlPlugin)[] = [...REQUIRED_FIELDS, 'icon', 'tags', 'database']
45+
46+
function validatePluginToml(pluginName: string, pluginPath: string): ValidationResult | null {
3247
const errors: ValidationError[] = []
3348
const tomlPath = path.join(pluginPath, 'plugin.toml')
3449

3550
if (!fs.existsSync(tomlPath)) {
36-
return errors
51+
return null
3752
}
3853

3954
let config: PluginToml
4055
try {
4156
config = toml.parse(fs.readFileSync(tomlPath, 'utf-8')) as PluginToml
4257
} catch (e) {
4358
errors.push({
44-
plugin: pluginName,
4559
field: 'plugin.toml',
46-
message: `解析失败: ${ e instanceof Error ? e.message : String(e) }`,
60+
message: `Parse failed: ${ e instanceof Error ? e.message : String(e) }`,
4761
})
48-
return errors
62+
return { plugin: pluginName, passed: false, config: null, errors }
4963
}
5064

5165
if (!config.plugin) {
5266
errors.push({
53-
plugin: pluginName,
5467
field: 'plugin',
55-
message: '缺少 [plugin] 配置块',
68+
message: 'Missing [plugin] section',
5669
})
57-
return errors
70+
return { plugin: pluginName, passed: false, config: null, errors }
5871
}
5972

60-
// 验证 tags
73+
// Validate required fields
74+
for (const field of REQUIRED_FIELDS) {
75+
const value = config.plugin[field]
76+
if (!value || (typeof value === 'string' && value.trim() === '')) {
77+
errors.push({
78+
field,
79+
message: 'Required field missing',
80+
})
81+
}
82+
}
83+
84+
// Validate tags
6185
if (config.plugin.tags && Array.isArray(config.plugin.tags)) {
6286
const invalidTags = config.plugin.tags.filter(
6387
tag => !VALID_TAGS.includes(tag.toLowerCase() as ValidTag)
6488
)
6589
if (invalidTags.length > 0) {
6690
errors.push({
67-
plugin: pluginName,
6891
field: 'tags',
69-
message: `包含无效的 tags 值`,
92+
message: 'Invalid values',
7093
invalidValues: invalidTags,
7194
})
7295
}
7396
}
7497

75-
// 验证 database
98+
// Validate database
7699
if (config.plugin.database && Array.isArray(config.plugin.database)) {
77100
const invalidDatabases = config.plugin.database.filter(
78101
db => !VALID_DATABASES.includes(db.toLowerCase() as ValidDatabase)
79102
)
80103
if (invalidDatabases.length > 0) {
81104
errors.push({
82-
plugin: pluginName,
83105
field: 'database',
84-
message: `包含无效的 database 值`,
106+
message: 'Invalid values',
85107
invalidValues: invalidDatabases,
86108
})
87109
}
88110
}
89111

90-
return errors
112+
return {
113+
plugin: pluginName,
114+
passed: errors.length === 0,
115+
config: config.plugin,
116+
errors,
117+
}
118+
}
119+
120+
function formatFieldRow(name: string, value: string | string[] | undefined, error?: ValidationError): string {
121+
if (Array.isArray(value) && value.length > 0) {
122+
const display = value.map(v => `\`${ v }\``).join(', ')
123+
const status = error
124+
? `❌ ${ error.message }: ${ error.invalidValues!.map(v => `\`${ v }\``).join(', ') }`
125+
: '✅'
126+
return `| \`${ name }\` | ${ display } | ${ status } |\n`
127+
}
128+
if (typeof value === 'string' && value.trim()) {
129+
return `| \`${ name }\` | ${ value } | ${ error ? `❌ ${ error.message }` : '✅' } |\n`
130+
}
131+
return `| \`${ name }\` | - | ${ error ? `❌ ${ error.message }` : '➖' } |\n`
132+
}
133+
134+
function generateReport(results: ValidationResult[]): string {
135+
const allPassed = results.every(r => r.passed)
136+
137+
let report = `## Plugin TOML Validation Report\n\n`
138+
report += `**Status**: ${ allPassed ? '✅ All Passed' : '❌ Validation Failed' }\n\n`
139+
140+
for (const result of results) {
141+
report += `### ${ result.passed ? '✅' : '❌' } \`${ result.plugin }\`\n\n`
142+
143+
if (!result.config) {
144+
result.errors.forEach(e => report += `> ⚠️ \`${ e.field }\`: ${ e.message }\n`)
145+
report += '\n'
146+
continue
147+
}
148+
149+
const errorMap = new Map(result.errors.map(e => [e.field, e]))
150+
report += `| Field | Value | Status |\n`
151+
report += `|-------|-------|--------|\n`
152+
for (const field of ALL_FIELDS) {
153+
report += formatFieldRow(field, result.config[field], errorMap.get(field))
154+
}
155+
report += '\n'
156+
}
157+
158+
if (!allPassed) {
159+
report += `### Reference\n\n`
160+
report += `| Type | Allowed Values |\n`
161+
report += `|------|----------------|\n`
162+
report += `| Tags | ${ VALID_TAGS.map(t => `\`${ t }\``).join(', ') } |\n`
163+
report += `| Database | ${ VALID_DATABASES.map(d => `\`${ d }\``).join(', ') } |\n`
164+
}
165+
166+
return report
91167
}
92168

93169
function main() {
94170
const pluginsDir = path.join(__dirname, 'plugins')
95-
const allErrors: ValidationError[] = []
171+
const results: ValidationResult[] = []
96172

97173
if (!fs.existsSync(pluginsDir)) {
98-
console.log('plugins 目录不存在')
174+
console.log('plugins directory not found')
99175
process.exit(0)
100176
}
101177

102-
// 支持命令行参数指定要验证的插件
103178
const args = process.argv.slice(2)
104179
let pluginDirs: string[]
105180

106181
if (args.length > 0) {
107-
// 验证指定的插件
108182
pluginDirs = args.filter(name => {
109183
const pluginPath = path.join(pluginsDir, name)
110184
return fs.existsSync(pluginPath)
111185
})
112186
} else {
113-
// 验证所有插件
114187
pluginDirs = fs.readdirSync(pluginsDir, { withFileTypes: true })
115-
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
116-
.map(entry => entry.name)
188+
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
189+
.map(entry => entry.name)
117190
}
118191

119-
console.log(`验证 ${ pluginDirs.length } 个插件...\n`)
120-
121192
for (const pluginName of pluginDirs) {
122193
const pluginPath = path.join(pluginsDir, pluginName)
123-
const errors = validatePluginToml(pluginName, pluginPath)
124-
allErrors.push(...errors)
194+
const result = validatePluginToml(pluginName, pluginPath)
195+
if (result) {
196+
results.push(result)
197+
}
125198
}
126199

127-
if (allErrors.length === 0) {
128-
console.log('所有插件验证通过')
129-
console.log(`\n允许的 tags: ${ VALID_TAGS.join(', ') }`)
130-
console.log(`允许的 database: ${ VALID_DATABASES.join(', ') }`)
131-
process.exit(0)
132-
}
200+
const passed = results.filter(r => r.passed).length
201+
const failed = results.filter(r => !r.passed).length
133202

134-
console.error('验证失败:\n')
135-
for (const error of allErrors) {
136-
console.error(`[${ error.plugin }] ${ error.field }: ${ error.message }`)
137-
if (error.invalidValues) {
138-
console.error(` 无效值: ${ error.invalidValues.join(', ') }`)
203+
// Write report file (for CI)
204+
const reportPath = process.env.VALIDATION_REPORT_PATH
205+
if (reportPath) {
206+
fs.writeFileSync(reportPath, generateReport(results), 'utf-8')
207+
console.log(`Done: ${ passed } passed, ${ failed } failed`)
208+
} else {
209+
console.log(`Validating ${ pluginDirs.length } plugins...\n`)
210+
for (const { plugin, passed: ok, errors } of results) {
211+
if (ok) {
212+
console.log(` ✅ ${ plugin }`)
213+
continue
214+
}
215+
console.error(` ❌ ${ plugin }`)
216+
errors.forEach(({ field, message, invalidValues }) =>
217+
console.error(` - ${ field }: ${ invalidValues ? `${ message }: ${ invalidValues.join(', ') }` : message }`)
218+
)
219+
}
220+
console.log(`\nResult: ${ passed } passed, ${ failed } failed`)
221+
if (failed > 0) {
222+
console.error(`\nAllowed tags: ${ VALID_TAGS.join(', ') }`)
223+
console.error(`Allowed database: ${ VALID_DATABASES.join(', ') }`)
139224
}
140225
}
141226

142-
console.error(`\n允许的 tags: ${ VALID_TAGS.join(', ') }`)
143-
console.error(`允许的 database: ${ VALID_DATABASES.join(', ') }`)
144-
process.exit(1)
227+
process.exit(failed > 0 ? 1 : 0)
145228
}
146229

147230
main()

0 commit comments

Comments
 (0)