From f8a82c4e9bcf6b30e543ed81e1289b8acb76688f Mon Sep 17 00:00:00 2001 From: Mohammed Noumaan Ahamed Date: Thu, 21 May 2026 13:53:44 +0530 Subject: [PATCH] Build: Add file type verification for CSS and JS directories --- .github/workflows/file-type-check.yml | 213 ++++++++++++++++++++++++ Gruntfile.js | 225 ++++++++++++++++++++++++++ 2 files changed, 438 insertions(+) create mode 100644 .github/workflows/file-type-check.yml diff --git a/.github/workflows/file-type-check.yml b/.github/workflows/file-type-check.yml new file mode 100644 index 0000000000000..ccb2719c95716 --- /dev/null +++ b/.github/workflows/file-type-check.yml @@ -0,0 +1,213 @@ +name: File Type Check + +on: + push: + branches: + - trunk + - '7.[1-9]' + - '[8-9].[0-9]' + paths: + # Inward check: files added to CSS/JS directories. + - 'src/wp-includes/css/**' + - 'src/wp-includes/js/**' + - 'src/wp-admin/css/**' + - 'src/wp-admin/js/**' + # Outward check: CSS/JS files placed outside designated directories. + - 'src/wp-includes/**/*.css' + - 'src/wp-includes/**/*.js' + - 'src/wp-admin/**/*.css' + - 'src/wp-admin/**/*.js' + # Confirm any changes to the workflow file itself. + - '.github/workflows/file-type-check.yml' + pull_request: + branches: + - trunk + - '7.[1-9]' + - '[8-9].[0-9]' + paths: + - 'src/wp-includes/css/**' + - 'src/wp-includes/js/**' + - 'src/wp-admin/css/**' + - 'src/wp-admin/js/**' + - 'src/wp-includes/**/*.css' + - 'src/wp-includes/**/*.js' + - 'src/wp-admin/**/*.css' + - 'src/wp-admin/**/*.js' + # Confirm any changes to the workflow file itself. + - '.github/workflows/file-type-check.yml' + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Verifies that CSS directories only contain CSS files and JS directories only + # contain JS files among version-controlled sources. + # + # Note: Many non-CSS/JS files exist in these directories as build artifacts + # (e.g., registry.php, *.asset.php, TinyMCE skins/fonts). These are all + # gitignored and never present in the checkout, so they do not need to be + # allow-listed here. The local Grunt verify:file-types task handles those. + # + # The only tracked non-CSS files are the SCSS sources in wp-admin/css/colors/. + check-file-types: + name: Verify file types in CSS/JS directories + runs-on: ubuntu-24.04 + permissions: + contents: read + if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} + timeout-minutes: 5 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Check for disallowed file types + run: | + violations="" + + ## + # src/wp-includes/css — only .css files allowed. + ## + if [ -d "src/wp-includes/css" ]; then + found=$(find src/wp-includes/css -type f \ + ! -name '*.css' \ + 2>/dev/null) + if [ -n "$found" ]; then + violations="${violations}${found}"$'\n' + fi + fi + + ## + # src/wp-includes/js — only .js files allowed. + ## + if [ -d "src/wp-includes/js" ]; then + found=$(find src/wp-includes/js -type f \ + ! -name '*.js' \ + 2>/dev/null) + if [ -n "$found" ]; then + violations="${violations}${found}"$'\n' + fi + fi + + ## + # src/wp-admin/css — only .css and .scss files allowed. + # .scss files are tracked sources for admin color schemes. + ## + if [ -d "src/wp-admin/css" ]; then + found=$(find src/wp-admin/css -type f \ + ! -name '*.css' \ + ! -name '*.scss' \ + 2>/dev/null) + if [ -n "$found" ]; then + violations="${violations}${found}"$'\n' + fi + fi + + ## + # src/wp-admin/js — only .js files allowed. + ## + if [ -d "src/wp-admin/js" ]; then + found=$(find src/wp-admin/js -type f \ + ! -name '*.js' \ + 2>/dev/null) + if [ -n "$found" ]; then + violations="${violations}${found}"$'\n' + fi + fi + + ## + # OUTWARD CHECK + # Ensure .css and .js files DO NOT exist outside their designated folders. + # Excluded directories that legitimately contain CSS/JS: + # - src/wp-includes/blocks/ : Gutenberg block-specific assets. + # - src/wp-includes/build/ : Routes/pages build system JS files. + ## + outward_found=$(find src/wp-includes src/wp-admin -type f \( -name '*.css' -o -name '*.js' \) \ + ! -path 'src/wp-includes/css/*' \ + ! -path 'src/wp-includes/js/*' \ + ! -path 'src/wp-admin/css/*' \ + ! -path 'src/wp-admin/js/*' \ + ! -path 'src/wp-includes/blocks/*' \ + ! -path 'src/wp-includes/build/*' \ + 2>/dev/null) + if [ -n "$outward_found" ]; then + violations="${violations}${outward_found}"$'\n' + fi + + # Report results. + if [ -n "$violations" ]; then + echo "::error::File type placement violations found." + echo "" + echo "The following files violate file type placement rules:" + echo "$violations" + echo "" + echo "Inward: CSS directories should only contain .css files and JS directories should only contain .js files." + echo "Outward: .css and .js files should only exist in their designated css/ and js/ directories." + echo "If a file is a legitimate exception, update the allow-list in both:" + echo " - .github/workflows/file-type-check.yml" + echo " - Gruntfile.js (verify:file-types task)" + exit 1 + fi + + echo "All files in CSS/JS directories have correct file types." + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ check-file-types ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ check-file-types, slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/Gruntfile.js b/Gruntfile.js index 8863d030627b8..a9462430927c1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -2017,6 +2017,7 @@ module.exports = function(grunt) { grunt.registerTask( 'verify:build', [ 'verify:old-files', 'verify:source-maps', + 'verify:file-types', ] ); /** @@ -2119,6 +2120,230 @@ module.exports = function(grunt) { } ); } ); + /** + * Verify file type placement rules: + * + * 1. Inward check: CSS directories should only contain CSS files and + * JS directories should only contain JS files. Third-party library + * assets and build-generated files are covered by an allow-list. + * + * 2. Outward check: CSS and JS files should only exist inside their + * designated directories, not scattered elsewhere in wp-includes/ + * or wp-admin/. Gutenberg block assets (wp-includes/blocks/) and + * the routes/pages build system (wp-includes/build/) are excluded + * as they legitimately contain CSS/JS files. + */ + grunt.registerTask( 'verify:file-types', function() { + var minimatch = require( 'minimatch' ); + var violations = []; + + /* + * ===================================================================== + * INWARD CHECK + * Ensures CSS/JS directories only contain their respective file types. + * ===================================================================== + * + * Each entry maps a directory (relative to SOURCE_DIR) to its expected + * file extension and a list of glob patterns (relative to that directory) + * for files that are legitimate exceptions to the rule. + * + * Note: Many of the allow-listed files below are gitignored build + * artifacts that only exist locally after running `build:dev`. They + * are never committed to the repository. The GitHub Actions workflow + * does not need these allow-list entries since it operates on a clean + * checkout that does not contain build artifacts. + */ + var inwardRules = [ + { + dir: 'wp-includes/css', + allowedExt: '.css', + allowList: [ + // Auto-generated style registry (gitignored build artifact). + 'dist/registry.php', + ], + }, + { + dir: 'wp-includes/js', + allowedExt: '.js', + allowList: [ + // TinyMCE PHP wrapper that serves concatenated JS (gitignored). + 'tinymce/wp-tinymce.php', + // Build-generated asset dependency manifests (gitignored). + 'dist/script-modules/**/*.asset.php', + // Build-generated script module registry (gitignored). + 'dist/script-modules/registry.php', + // TinyMCE skin CSS files (gitignored). + 'tinymce/skins/**/*.css', + // TinyMCE skin images (gitignored). + 'tinymce/skins/**/*.png', + 'tinymce/skins/**/*.gif', + 'tinymce/skins/**/*.svg', + // TinyMCE font files (gitignored). + 'tinymce/skins/**/fonts/*.ttf', + 'tinymce/skins/**/fonts/*.woff', + 'tinymce/skins/**/fonts/*.eot', + // TinyMCE compat plugin CSS (gitignored). + 'tinymce/plugins/compat3x/css/*.css', + // License files (gitignored). + 'tinymce/license.txt', + 'swfupload/license.txt', + 'plupload/license.txt', + // CodeMirror CSS bundled with JS (gitignored). + 'codemirror/codemirror.min.css', + // Jcrop library assets (gitignored). + 'jcrop/jquery.Jcrop.min.css', + 'jcrop/Jcrop.gif', + // Crop library assets (gitignored). + 'crop/cropper.css', + 'crop/marqueeVert.gif', + 'crop/marqueeHoriz.gif', + // Media element player assets (gitignored). + 'mediaelement/*.css', + 'mediaelement/*.png', + 'mediaelement/*.svg', + // Thickbox library assets (gitignored). + 'thickbox/thickbox.css', + 'thickbox/*.gif', + 'thickbox/*.png', + // imgAreaSelect library assets (gitignored). + 'imgareaselect/imgareaselect.css', + 'imgareaselect/*.gif', + ], + }, + { + dir: 'wp-admin/css', + allowedExt: '.css', + allowList: [ + // SCSS source files for admin color schemes. + 'colors/**/*.scss', + ], + }, + { + dir: 'wp-admin/js', + allowedExt: '.js', + allowList: [], + }, + ]; + + inwardRules.forEach( function( rule ) { + var baseDir = SOURCE_DIR + rule.dir; + + if ( ! fs.existsSync( baseDir ) ) { + return; + } + + // Recursively find all files in the directory. + var allFiles = glob.sync( '**/*', { + cwd: baseDir, + nodir: true, + dot: false, + } ); + + allFiles.forEach( function( file ) { + // Skip files that already have the correct extension. + var ext = path.extname( file ); + if ( ext === rule.allowedExt || ext === '.min' + rule.allowedExt.slice( 1 ) ) { + return; + } + + // Handle compound extensions like `.min.css` or `.min.js`. + if ( file.endsWith( '.min' + rule.allowedExt ) ) { + return; + } + + // Check if the file matches any allow-list pattern. + var isAllowed = rule.allowList.some( function( pattern ) { + return minimatch( file, pattern, { matchBase: false } ); + } ); + + if ( ! isAllowed ) { + violations.push( '(inward) ' + baseDir + '/' + file ); + } + } ); + } ); + + /* + * ===================================================================== + * OUTWARD CHECK + * Ensures CSS/JS files only exist inside their designated directories. + * ===================================================================== + * + * Directories that legitimately contain CSS/JS files outside the + * designated folders: + * - wp-includes/blocks/ : Gutenberg block-specific assets. + * - wp-includes/build/ : Routes/pages build system JS files. + */ + var outwardDirs = [ + SOURCE_DIR + 'wp-includes', + SOURCE_DIR + 'wp-admin', + ]; + + // Directories where CSS/JS files are expected. + var designatedDirs = [ + SOURCE_DIR + 'wp-includes/css', + SOURCE_DIR + 'wp-includes/js', + SOURCE_DIR + 'wp-admin/css', + SOURCE_DIR + 'wp-admin/js', + ]; + + // Directories that legitimately contain CSS/JS outside the designated folders. + var outwardExcludeDirs = [ + SOURCE_DIR + 'wp-includes/blocks', + SOURCE_DIR + 'wp-includes/build', + ]; + + outwardDirs.forEach( function( searchDir ) { + if ( ! fs.existsSync( searchDir ) ) { + return; + } + + var allFiles = glob.sync( '**/*.{css,js}', { + cwd: searchDir, + nodir: true, + dot: false, + } ); + + allFiles.forEach( function( file ) { + var fullPath = searchDir + '/' + file; + + // Skip files inside designated CSS/JS directories. + var inDesignated = designatedDirs.some( function( dir ) { + return fullPath.indexOf( dir + '/' ) === 0; + } ); + + if ( inDesignated ) { + return; + } + + // Skip files inside excluded directories (blocks, build). + var inExcluded = outwardExcludeDirs.some( function( dir ) { + return fullPath.indexOf( dir + '/' ) === 0; + } ); + + if ( inExcluded ) { + return; + } + + violations.push( '(outward) ' + fullPath ); + } ); + } ); + + if ( violations.length > 0 ) { + grunt.log.errorlns( 'File type placement violations found:' ); + violations.forEach( function( entry ) { + grunt.log.errorlns( ' ' + entry ); + } ); + grunt.fatal( + violations.length + ' file(s) with placement violations found.\n' + + 'Inward: CSS directories should only contain .css files and JS directories should only contain .js files.\n' + + 'Outward: .css and .js files should only exist in their designated css/ and js/ directories.\n' + + 'If a file is a legitimate exception, update the verify:file-types task in Gruntfile.js.' + ); + } + + grunt.log.ok( 'All CSS/JS files are correctly placed.' ); + } ); + grunt.registerTask( 'routes:setup', 'Reads the routes registry and configures the copy:routes task.', function() { const registryPath = 'gutenberg/build/routes/registry.php'; let registryContent;