Files
robot-plugins/.github/workflows/validate.yml

248 lines
8.4 KiB
YAML

name: Validate Plugins
on:
push:
branches: ["main", "develop"]
pull_request:
branches: ["main"]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: |
npm install -g ajv-cli
npm install -g jsonlint
- name: Validate JSON syntax
run: |
echo "Validating repository.json..."
jsonlint repository.json
echo "Validating plugins/index.json..."
jsonlint plugins/index.json
echo "Validating plugin files..."
for file in plugins/*.json; do
if [ "$file" != "plugins/index.json" ]; then
echo "Validating $file..."
jsonlint "$file"
fi
done
- name: Check plugin index consistency
run: |
echo "Checking plugin index consistency..."
node -e "
const fs = require('fs');
const index = JSON.parse(fs.readFileSync('plugins/index.json', 'utf8'));
const files = fs.readdirSync('plugins').filter(f => f.endsWith('.json') && f !== 'index.json');
console.log('Index files:', index);
console.log('Actual files:', files);
const missing = files.filter(f => !index.includes(f));
const extra = index.filter(f => !files.includes(f));
if (missing.length > 0) {
console.error('Files missing from index:', missing);
process.exit(1);
}
if (extra.length > 0) {
console.error('Extra files in index:', extra);
process.exit(1);
}
console.log('Plugin index is consistent ✓');
"
- name: Validate plugin schemas
run: |
echo "Validating plugin schemas..."
node -e "
const fs = require('fs');
// Basic plugin schema validation
function validatePlugin(pluginPath) {
const plugin = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
const errors = [];
// Required fields
const required = ['robotId', 'name', 'platform', 'version', 'pluginApiVersion', 'hriStudioVersion', 'trustLevel', 'category'];
for (const field of required) {
if (!plugin[field]) {
errors.push(\`Missing required field: \${field}\`);
}
}
// Validate trustLevel enum
if (plugin.trustLevel && !['official', 'verified', 'community'].includes(plugin.trustLevel)) {
errors.push(\`Invalid trustLevel: \${plugin.trustLevel}\`);
}
// Validate actions
if (plugin.actions && Array.isArray(plugin.actions)) {
plugin.actions.forEach((action, i) => {
if (!action.id) errors.push(\`Action \${i}: missing id\`);
if (!action.name) errors.push(\`Action \${i}: missing name\`);
if (!action.category) errors.push(\`Action \${i}: missing category\`);
if (action.category && !['movement', 'interaction', 'sensors', 'logic'].includes(action.category)) {
errors.push(\`Action \${i}: invalid category \${action.category}\`);
}
if (!action.parameterSchema) errors.push(\`Action \${i}: missing parameterSchema\`);
});
}
return errors;
}
const index = JSON.parse(fs.readFileSync('plugins/index.json', 'utf8'));
let hasErrors = false;
for (const pluginFile of index) {
console.log(\`Validating \${pluginFile}...\`);
const errors = validatePlugin(\`plugins/\${pluginFile}\`);
if (errors.length > 0) {
console.error(\`Errors in \${pluginFile}:\`);
errors.forEach(error => console.error(\` - \${error}\`));
hasErrors = true;
} else {
console.log(\` ✓ Valid\`);
}
}
if (hasErrors) {
process.exit(1);
}
console.log('All plugins are valid ✓');
"
- name: Check asset references
run: |
echo "Checking asset references..."
node -e "
const fs = require('fs');
const path = require('path');
function checkAssets(pluginPath) {
const plugin = JSON.parse(fs.readFileSync(pluginPath, 'utf8'));
const errors = [];
if (plugin.assets) {
const checkAsset = (assetPath, description) => {
if (assetPath && assetPath.startsWith('assets/')) {
const fullPath = assetPath;
if (!fs.existsSync(fullPath)) {
errors.push(\`\${description}: \${assetPath} not found\`);
}
}
};
checkAsset(plugin.assets.thumbnailUrl, 'Thumbnail');
if (plugin.assets.images) {
checkAsset(plugin.assets.images.main, 'Main image');
checkAsset(plugin.assets.images.logo, 'Logo');
if (plugin.assets.images.angles) {
Object.entries(plugin.assets.images.angles).forEach(([angle, path]) => {
checkAsset(path, \`\${angle} angle image\`);
});
}
}
}
return errors;
}
const index = JSON.parse(fs.readFileSync('plugins/index.json', 'utf8'));
let hasErrors = false;
for (const pluginFile of index) {
console.log(\`Checking assets for \${pluginFile}...\`);
const errors = checkAssets(\`plugins/\${pluginFile}\`);
if (errors.length > 0) {
console.error(\`Asset errors in \${pluginFile}:\`);
errors.forEach(error => console.error(\` - \${error}\`));
hasErrors = true;
} else {
console.log(\` ✓ All assets found\`);
}
}
if (hasErrors) {
console.warn('Some assets are missing - this may cause display issues');
} else {
console.log('All assets are available ✓');
}
"
- name: Validate repository metadata
run: |
echo "Validating repository metadata..."
node -e "
const fs = require('fs');
const repo = JSON.parse(fs.readFileSync('repository.json', 'utf8'));
const index = JSON.parse(fs.readFileSync('plugins/index.json', 'utf8'));
// Check plugin count matches
const actualCount = index.length;
const reportedCount = repo.stats?.plugins || 0;
if (actualCount !== reportedCount) {
console.error(\`Plugin count mismatch: reported \${reportedCount}, actual \${actualCount}\`);
process.exit(1);
}
console.log(\`Plugin count is correct: \${actualCount} ✓\`);
// Check required repository fields
const required = ['id', 'name', 'apiVersion', 'pluginApiVersion', 'trust'];
for (const field of required) {
if (!repo[field]) {
console.error(\`Missing required repository field: \${field}\`);
process.exit(1);
}
}
console.log('Repository metadata is valid ✓');
"
- name: Test web interface
run: |
echo "Testing web interface..."
# Start a simple HTTP server
python3 -m http.server 8000 &
SERVER_PID=$!
# Wait for server to start
sleep 2
# Test that index.html loads
curl -f http://localhost:8000/index.html > /dev/null
# Test that repository.json is accessible
curl -f http://localhost:8000/repository.json > /dev/null
# Test that plugins/index.json is accessible
curl -f http://localhost:8000/plugins/index.json > /dev/null
# Clean up
kill $SERVER_PID
echo "Web interface test passed ✓"