!predeploy.yml
1 name: Predeploy 2 # Optimized workflow to format and validate code 3 # All other workflows should depend on this workflow to ensure code quality before deployment 4 on: 5 push: 6 branches: 7 - js 8 paths: 9 - 'app/**/*.js' 10 - 'app/**/*.css' 11 - 'app/*.json' 12 - 'app/index.html' 13 - '.github/workflows/*deploy*' 14 - '.github/workflows/*sync*' 15 - 'biome.json' 16 jobs: 17 predeploy: 18 name: โ๏ธ 19 runs-on: ubuntu-latest 20 permissions: 21 contents: write 22 steps: 23 # ===== SETUP STEPS ===== 24 - name: ๐ Checkout 25 uses: actions/checkout@v4 26 with: 27 fetch-depth: 1 # Shallow clone for faster checkout 28 - name: ๐ง Node.js 29 uses: actions/setup-node@v4 30 with: 31 node-version: '20' 32 33 # ===== VALIDATION STEPS ===== 34 - name: ๐ JS Syntax Check 35 run: | 36 echo "Running Node.js syntax validation on app/ directory..." 37 set -e 38 errors=0 39 summary="" 40 41 # Check if any JavaScript files exist in the app directory 42 js_files_count=$(find app -type f -name "*.js" | wc -l) 43 if [ "$js_files_count" -eq 0 ]; then 44 echo "No JavaScript files found in app/ directory. Skipping JS Syntax Check." 45 exit 0 46 fi 47 echo "Found $js_files_count JavaScript files to check." 48 49 js_files=$(find app -type f -name "*.js") 50 for file in $js_files; do 51 echo "Checking $file" 52 if ! output=$(node --check "$file" 2>&1); then 53 errors=$((errors+1)) 54 summary+="$file:\n$output\n\n" 55 fi 56 done 57 if [ "$errors" -gt 0 ]; then 58 echo -e "โ ๏ธ Syntax errors found in $errors files:\n" 59 echo -e "$summary" 60 exit 1 61 else 62 echo "โ No syntax errors detected in JavaScript files." 63 fi 64 65 - name: ๐จ CSS Syntax Check and Fix 66 run: | 67 echo "Running CSS syntax validation and auto-fix on app/css/ directory..." 68 if [ ! -d "app/css" ]; then 69 echo "app/css directory not found. Skipping CSS Syntax Check." 70 exit 0 71 fi 72 css_files=$(find app/css -type f -name "*.css") 73 if [ -z "$css_files" ]; then 74 echo "No CSS files found in app/css/. Skipping CSS Syntax Check." 75 exit 0 76 fi 77 npm install --no-save stylelint stylelint-config-standard 78 cat > .stylelintrc.json << 'EOF' 79 { 80 "extends": "stylelint-config-standard", 81 "rules": { 82 "no-descending-specificity": null, 83 "selector-class-pattern": null, 84 "selector-id-pattern": null, 85 "comment-empty-line-before": null 86 } 87 } 88 EOF 89 echo "Fixing CSS files with stylelint..." 90 for file in $css_files; do 91 echo "Checking and fixing $file" 92 npx stylelint "$file" --fix --formatter=string || true 93 done 94 set -e 95 errors=0 96 summary="" 97 for file in $css_files; do 98 if ! output=$(npx stylelint "$file" --formatter=string 2>&1); then 99 errors=$((errors+1)) 100 summary+="$file:\n$output\n\n" 101 fi 102 done 103 rm -f .stylelintrc.json 104 if [ "$errors" -gt 0 ]; then 105 echo -e "โ ๏ธ Syntax errors still found in $errors CSS files after fixing:\n" 106 echo -e "$summary" 107 echo "::warning::CSS syntax issues detected after auto-fix, but continuing workflow" 108 else 109 echo "โ No syntax errors detected in CSS files after fixing." 110 fi 111 112 - name: ๐จ CSS Fix Commit 113 run: | 114 git config --global user.name "dtub[bot]" 115 git config --global user.email "209926867+dtub[bot]@users.noreply.github.com" 116 git checkout -- package-lock.json 2>/dev/null || true 117 git checkout -- package.json 2>/dev/null || true 118 # Only add changes in app/css to be specific 119 git add app/css/ || true 120 if [[ -n $(git diff --cached --name-only app/css/) ]]; then 121 echo "CSS changes detected, committing..." 122 git commit -m "๐จ Auto-fix CSS with stylelint" || echo "Commit failed, but continuing" 123 git push --force-with-lease || echo "Push failed, but continuing" 124 echo "CSS fixes committed and pushed." 125 else 126 echo "No CSS changes to commit after fixing." 127 fi 128 129 - name: ๐ JSON Validation 130 run: | 131 echo "Validating app/videos.json structure..." 132 if [ ! -f "app/videos.json" ]; then 133 echo "::error::app/videos.json file not found!" 134 exit 1 135 fi 136 if ! output=$(node -e "try { JSON.parse(require('fs').readFileSync('app/videos.json', 'utf8')); console.log('โ JSON syntax is valid.'); } catch(e) { console.error('โ JSON syntax error: ' + e.message); process.exit(1); }" 2>&1); then 137 echo "::error::$output" 138 exit 1 139 fi 140 node -e " 141 const fs = require('fs'); 142 const videos = JSON.parse(fs.readFileSync('app/videos.json', 'utf8')); 143 if (!Array.isArray(videos)) { 144 console.error('โ videos.json must be an array'); 145 process.exit(1); 146 } 147 let hasErrors = false; 148 videos.forEach((video, i) => { 149 if (typeof video !== 'object' || video === null) { 150 console.error(`โ Item at index ${i} is not an object`); 151 hasErrors = true; return; 152 } 153 const requiredFields = ['index', 'cid', 'name']; 154 for (const field of requiredFields) { 155 if (!(field in video)) { 156 console.error(`โ Item at index ${i} is missing required field: ${field}`); 157 hasErrors = true; 158 } 159 } 160 if ('index' in video && typeof video.index !== 'number') { console.error(`โ Item at index ${i}: 'index' must be a number`); hasErrors = true; } 161 if ('cid' in video && typeof video.cid !== 'string') { console.error(`โ Item at index ${i}: 'cid' must be a string`); hasErrors = true; } 162 if ('name' in video && typeof video.name !== 'string') { console.error(`โ Item at index ${i}: 'name' must be a string`); hasErrors = true; } 163 if ('altcid' in video && typeof video.altcid !== 'string') { console.error(`โ Item at index ${i}: 'altcid' must be a string`); hasErrors = true; } 164 }); 165 if (hasErrors) process.exit(1); 166 else console.log(\`โ videos.json structure is valid (\${videos.length} videos).\`); 167 " 168 169 - name: ๐ HTML Validation 170 run: | 171 echo "Validating app/index.html structure..." 172 if [ ! -f "app/index.html" ]; then 173 echo "::error::app/index.html file not found!" 174 exit 1 175 fi 176 npm install --no-save html-validate 177 cat > .htmlvalidate.json << 'EOF' 178 { 179 "extends": ["html-validate:recommended"], 180 "elements": ["html5", {"img": {"attributes": {"src": {"required": false}, "data-src": {"required": false}, "alt": {"required": false}}, "permittedContent": ["flow", "phrasing", "embedded", "interactive"], "requiredAttributes": [] }}], 181 "rules": {"attribute-boolean-style": "off", "no-dup-id": "error", "void-style": "off", "doctype-style": "off", "no-trailing-whitespace": "off", "prefer-native-element": "off", "svg-focusable": "off", "input-attributes": "off", "require-sri": "off", "wcag/h37": "off", "element-required-attributes": "error"} 182 } 183 EOF 184 if ! output=$(npx html-validate app/index.html 2>&1); then 185 echo "::error::HTML validation failed:" 186 echo "$output" 187 rm -f .htmlvalidate.json 188 exit 1 189 else 190 echo "โ app/index.html structure is valid." 191 rm -f .htmlvalidate.json 192 fi 193 echo "Checking for duplicate IDs in app/index.html..." 194 node -e " 195 const fs = require('fs'); const html = fs.readFileSync('app/index.html', 'utf8'); 196 const idRegex = /id=['\"]([^'\"]+)['\"]|id=([^ >]+)/g; const ids = []; let match; 197 while ((match = idRegex.exec(html)) !== null) { ids.push(match[1] || match[2]); } 198 const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index); 199 const uniqueDuplicates = [...new Set(duplicates)]; 200 if (uniqueDuplicates.length > 0) { 201 console.error('โ Duplicate IDs found in app/index.html:'); 202 uniqueDuplicates.forEach(id => console.error(\` - '\${id}' is used multiple times\`)); 203 process.exit(1); 204 } else { console.log('โ No duplicate IDs found in app/index.html.'); } 205 " 206 echo "Checking for required elements in app/index.html..." 207 node -e " 208 const fs = require('fs'); const html = fs.readFileSync('app/index.html', 'utf8'); 209 let hasErrors = false; 210 const requiredMetaTags = [ 211 { name: 'charset', pattern: /<meta\\s+charset=['\"]UTF-8['\"]/ }, 212 { name: 'viewport', pattern: /<meta\\s+content=['\"]width=device-width/ }, 213 { name: 'description', pattern: /<meta\\s+content=['\"].*?['\"]\\s+name=['\"]description['\"]/ } 214 ]; 215 requiredMetaTags.forEach(tag => { if (!tag.pattern.test(html)) { console.error(\`โ Required meta tag missing: \${tag.name}\`); hasErrors = true; }}); 216 const requiredResources = [ 217 { name: 'CSS stylesheets', pattern: /<link\\s+href=['\"]css\\/.*?\\.css['\"]\\s+rel=['\"]stylesheet['\"]/ }, 218 { name: 'Main app.js script', pattern: /<script\\s+.*?src=['\"]app\\.js['\"]/ } 219 ]; 220 requiredResources.forEach(resource => { if (!resource.pattern.test(html)) { console.error(\`โ Required resource missing: \${resource.name}\`); hasErrors = true; }}); 221 if (!/<video[^>]*id=['\"]video-player['\"]/.test(html)) { console.error('โ Required video player element missing'); hasErrors = true; } 222 if (hasErrors) process.exit(1); else console.log('โ All required elements found in app/index.html.'); 223 " 224 - name: โ Verify Biome Setup 225 run: | 226 echo "Biome version:" 227 npx --no-update-notifier -y @biomejs/biome --version 228 if [ -f "biome.json" ]; then 229 echo "Found biome.json configuration" 230 echo "// Test file for Biome config validation" > biome-test.js 231 npx --no-update-notifier -y @biomejs/biome check biome-test.js || echo "Biome config validation issues found, but continuing. This might affect formatting/linting." 232 rm biome-test.js 233 else 234 echo "No biome.json found. Biome will use default settings." 235 fi 236 237 # ===== CODE TRANSFORMATION STEPS ===== 238 # Step 1: Strip comments (least invasive) 239 - name: ๐จ๏ธ Strip Single-Line Comments (Preserving JSDoc) and Commit 240 run: | 241 # Check if any JavaScript files exist in the app directory 242 js_files_count=$(find app -type f -name "*.js" | wc -l) 243 if [ "$js_files_count" -eq 0 ]; then 244 echo "No JavaScript files found in app/ directory. Skipping comment stripping." 245 exit 0 246 fi 247 echo "Found $js_files_count JavaScript files to process." 248 249 npm install --no-save strip-comments@latest 250 251 cat > strip-comments.mjs << 'EOF' 252 import fs from 'fs'; 253 import path from 'path'; 254 import stripComments from 'strip-comments'; 255 256 const directories = ['app']; 257 let filesProcessed = 0; 258 let filesChanged = 0; 259 260 function processDirectory(dir) { 261 try { 262 const entries = fs.readdirSync(dir, { withFileTypes: true }); 263 for (const entry of entries) { 264 const fullPath = path.join(dir, entry.name); 265 if (entry.isDirectory()) { 266 processDirectory(fullPath); 267 } else if (entry.isFile() && path.extname(entry.name) === '.js') { 268 processFile(fullPath); 269 } 270 } 271 } catch (err) { 272 console.error(`Error reading directory ${dir}: ${err.message}`); 273 // Propagate error by re-throwing or exiting, so CI fails 274 throw err; 275 } 276 } 277 278 function processFile(filePath) { 279 try { 280 const originalContent = fs.readFileSync(filePath, 'utf8'); 281 const strippedContent = stripComments(originalContent, { 282 line: true, // Remove line comments (//) 283 block: false, // Keep block comments (/* */) 284 preserveJSDoc: true // Keep JSDoc comments (/** */) 285 }); 286 287 if (originalContent !== strippedContent) { 288 fs.writeFileSync(filePath, strippedContent, 'utf8'); 289 filesChanged++; 290 console.log(`Stripped comments from: ${filePath}`); 291 } 292 filesProcessed++; 293 } catch (err) { 294 console.error(`Error processing file ${filePath}: ${err.message}`); 295 throw err; // Propagate error 296 } 297 } 298 299 console.log('Starting comment stripping process...'); 300 try { 301 // Check if any JavaScript files exist in the directories 302 let jsFilesExist = false; 303 for (const dir of directories) { 304 if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) { 305 // Recursively check for JS files 306 const checkForJsFiles = (directory) => { 307 const entries = fs.readdirSync(directory, { withFileTypes: true }); 308 for (const entry of entries) { 309 const fullPath = path.join(directory, entry.name); 310 if (entry.isDirectory()) { 311 if (checkForJsFiles(fullPath)) return true; 312 } else if (entry.isFile() && path.extname(entry.name) === '.js') { 313 return true; 314 } 315 } 316 return false; 317 }; 318 319 if (checkForJsFiles(dir)) { 320 jsFilesExist = true; 321 break; 322 } 323 } 324 } 325 326 if (!jsFilesExist) { 327 console.log('No JavaScript files found in the specified directories. Exiting with code 2 (no changes).'); 328 process.exit(2); // No JS files found, exit with code 2 329 } 330 331 // Process directories if JS files exist 332 directories.forEach(dir => { 333 if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) { 334 processDirectory(dir); 335 } else { 336 console.warn(`Directory specified for stripping ('${dir}') not found or is not a directory. Skipping this directory.`); 337 } 338 }); 339 340 console.log(`Comment stripping completed. Processed ${filesProcessed} files. Changed ${filesChanged} files.`); 341 if (filesChanged > 0) { 342 process.exit(0); // Success, changes made 343 } else { 344 process.exit(2); // Success, no changes made 345 } 346 } catch (error) { 347 console.error('Comment stripping script failed overall:', error); 348 process.exit(1); // Generic error 349 } 350 EOF 351 352 # Run the comment stripping script and capture its exit code 353 node strip-comments.mjs || true 354 script_exit_code=$? 355 rm strip-comments.mjs 356 357 # Handle different exit codes from the script 358 if [ "$script_exit_code" -eq 0 ]; then 359 # Changes were made (exit code 0) 360 git config --global user.name "dtub[bot]" 361 git config --global user.email "209926867+dtub[bot]@users.noreply.github.com" 362 git checkout -- package-lock.json 2>/dev/null || true 363 git checkout -- package.json 2>/dev/null || true 364 365 echo "Staging changed JavaScript files..." 366 find app -type f -name "*.js" -print0 | xargs -0 git add 367 368 if [[ -n $(git diff --cached --name-only) ]]; then 369 echo "JS changes detected after stripping comments, committing..." 370 git commit -m "๐จ๏ธ Auto-strip comments" || echo "Commit failed, but continuing (commit step)" 371 git push --force-with-lease || echo "Push failed, but continuing (push step)" 372 echo "Comment stripping changes committed and pushed." 373 else 374 echo "No actual file changes staged to commit after stripping comments, though script indicated changes." 375 fi 376 elif [ "$script_exit_code" -eq 2 ]; then 377 # No changes made (exit code 2) - this is a success case, not an error 378 echo "No changes made by comment stripping script. Continuing workflow." 379 elif [ "$script_exit_code" -eq 1 ]; then 380 # Only treat exit code 1 as an actual error 381 echo "::warning::Error occurred during comment stripping. Check script logs, but continuing workflow." 382 else 383 # Any other unexpected exit code 384 echo "::warning::Unexpected exit code from comment stripping script: $script_exit_code. Continuing workflow." 385 fi 386 387 # Step 2: Format code (moderate changes) 388 - name: ๐ ๐ป Biome Format and Commit 389 run: | 390 # Check if any JavaScript files exist in the app directory 391 js_files_count=$(find app -type f -name "*.js" | wc -l) 392 if [ "$js_files_count" -eq 0 ]; then 393 echo "No JavaScript files found in app/ directory. Skipping Biome format." 394 exit 0 395 fi 396 echo "Found $js_files_count JavaScript files to process." 397 echo "Running Biome formatter..." 398 npx --no-update-notifier -y @biomejs/biome format --write app/ || true # Continue even on error to commit partial fixes 399 git config --global user.name "dtub[bot]" 400 git config --global user.email "209926867+dtub[bot]@users.noreply.github.com" 401 git checkout -- package-lock.json 2>/dev/null || true 402 git checkout -- package.json 2>/dev/null || true 403 git add -A app/ || true # Biome might format more than just JS, e.g. JSON if configured 404 if [[ -n $(git diff --cached --name-only) ]]; then 405 echo "Changes detected after Biome format, committing..." 406 git commit -m "๐ ๐ป Auto-format with Biome" || echo "Commit failed, but continuing" 407 git push --force-with-lease || echo "Push failed, but continuing" 408 echo "Formatting changes committed and pushed." 409 else 410 echo "No changes to commit after Biome formatting." 411 fi 412 413 # Step 3: Lint code (most invasive changes) 414 - name: ๐ชฎ Biome Lint and Commit 415 run: | 416 # Check if any JavaScript files exist in the app directory 417 js_files_count=$(find app -type f -name "*.js" | wc -l) 418 if [ "$js_files_count" -eq 0 ]; then 419 echo "No JavaScript files found in app/ directory. Skipping Biome lint." 420 exit 0 421 fi 422 echo "Found $js_files_count JavaScript files to process." 423 echo "Running Biome linter with --write --unsafe flag..." 424 npx --no-update-notifier -y @biomejs/biome lint --write --unsafe app/ || true # Continue even on error 425 git config --global user.name "dtub[bot]" 426 git config --global user.email "209926867+dtub[bot]@users.noreply.github.com" 427 git checkout -- package-lock.json 2>/dev/null || true 428 git checkout -- package.json 2>/dev/null || true 429 git add -A app/ || true # Biome might lint/fix more than just JS 430 if [[ -n $(git diff --cached --name-only) ]]; then 431 echo "Changes detected after Biome lint, committing..." 432 git commit -m "๐ชฎ Auto-lint with Biome" || echo "Commit failed, but continuing" 433 git push --force-with-lease || echo "Push failed, but continuing" 434 echo "Linting changes committed and pushed." 435 else 436 echo "No changes to commit after Biome linting." 437 fi 438 439 # ===== CLEANUP STEPS ===== 440 - name: ๐งน Cleanup 441 if: always() 442 run: | 443 rm -f *.sh .htmlvalidate.json .stylelintrc.json biome-test.js 444 echo "Cleanup completed."