/ .github / workflows / !predeploy.yml
!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."