diff --git a/.gitattributes b/.gitattributes index 39e21f3e5e..2186c8f749 100644 --- a/.gitattributes +++ b/.gitattributes @@ -36,3 +36,5 @@ /modules/**/readme.txt export-ignore /modules/**/.wordpress-org export-ignore + +/plugins/**/.wordpress-org export-ignore diff --git a/.github/workflows/deploy-standalone-plugins.yml b/.github/workflows/deploy-standalone-plugins.yml index 846adacb04..bc5b36f840 100644 --- a/.github/workflows/deploy-standalone-plugins.yml +++ b/.github/workflows/deploy-standalone-plugins.yml @@ -1,6 +1,18 @@ name: Deploy standalone plugins to WordPress.org on: + # TODO The pull_request will be removed once the workflow is tested. + pull_request: + branches: + - trunk + - 'release/**' + - 'feature/**' + paths: + - '.github/workflows/deploy-standalone-plugins.yml' + types: + - opened + - reopened + - synchronize release: types: [published] workflow_dispatch: @@ -28,6 +40,11 @@ jobs: cache: npm - name: Install npm dependencies run: npm ci + - name: Get directory + id: get-plugin-directory + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + echo "directory=$(node ./bin/plugin/cli.js get-plugin-dir --slug=${{ inputs.slug }})" >> $GITHUB_OUTPUT - name: Get plugin version id: get-version if: ${{ github.event_name == 'workflow_dispatch' }} @@ -40,7 +57,7 @@ jobs: result=$(echo "${{ steps.get-version.outputs.version }}" | awk '/^(\*|[0-9]+(\.[0-9]+){0,2}(-[a-zA-Z0-9.]+)?)$/ {print "Matched"}') if [[ -n "$result" ]]; then # Set the manual input values in JSON format for use in the matrix. - echo "matrix={\"include\":[{\"slug\":\"${{ inputs.slug }}\",\"version\":\"${{ steps.get-version.outputs.version }}\",\"dry-run\":\"${{ inputs.dry-run }}\"}]}" >> $GITHUB_OUTPUT + echo "matrix={\"include\":[{\"slug\":\"${{ inputs.slug }}\",\"version\":\"${{ steps.get-version.outputs.version }}\",\"directory\":\"${{ steps.get-plugin-directory.outputs.directory }}\",\"dry-run\":\"true\"}]}" >> $GITHUB_OUTPUT else echo "The ${{ inputs.slug }} module slug is missing in the file plugins.json." exit 1 @@ -48,9 +65,11 @@ jobs: else # Load the JSON file and parse from "{name: {slug, version}, ...}" to "include: [{ name, slug, version }, ...]" # for use in the matrix. + # For plugins, the "version" parameter is not included here; it will dynamically get it in its own job. + # The "dry-run" parameter is included here to set the deployment mode. # When running the manual (workflow_dispatch) workflow, this value will be set from manual input type. - echo "matrix="$(jq -c '{include:[.modules | to_entries[] | {name:.key,slug:.value.slug,version:.value.version,"dry-run":false }]}' plugins.json) >> $GITHUB_OUTPUT + echo "matrix=$(jq -c '{include: ([.modules | to_entries[] | {name:.key, slug: .value.slug, version: .value.version, directory: "build", "dry-run": true}] + ([.plugins[] | {name:. , slug:. , directory: "plugins", "dry-run": true}]))}' plugins.json)" >> $GITHUB_OUTPUT fi deploy: name: Deploy Plugin @@ -69,15 +88,28 @@ jobs: - name: Install npm dependencies run: npm ci - name: Building standalone plugins + if: ${{ matrix.directory != 'plugins' }} run: npm run build-plugins + - name: Set version + id: set_version + run: | + if [ -z "${{ matrix.version }}" ]; then + echo "version=$(node ./bin/plugin/cli.js get-plugin-version --slug=${{ matrix.slug }})" >> $GITHUB_OUTPUT + else + echo "version=${{ matrix.version }})" >> $GITHUB_OUTPUT + fi - name: Deploy Standalone Plugin - ${{ matrix.slug }} uses: 10up/action-wordpress-plugin-deploy@stable with: dry-run: ${{ matrix.dry-run }} env: - SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} - SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + # TODO Once the workflow is tested, we will remove the comment and use the secret SVN access. + #SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + #SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + # TODO Once the workflow is tested, we will remove this test credential. + SVN_PASSWORD: SVN_PASSWORD + SVN_USERNAME: SVN_USERNAME SLUG: ${{ matrix.slug }} - VERSION: ${{ matrix.version }} - BUILD_DIR: ./build/${{ matrix.slug }} - ASSETS_DIR: ./build/${{ matrix.slug }}/.wordpress-org + VERSION: ${{ steps.set_version.outputs.version }} + BUILD_DIR: ./${{ matrix.directory }}/${{ matrix.slug }} + ASSETS_DIR: ./${{ matrix.directory }}/${{ matrix.slug }}/.wordpress-org diff --git a/bin/plugin/cli.js b/bin/plugin/cli.js index 463f16ce2d..04dabfd2c2 100755 --- a/bin/plugin/cli.js +++ b/bin/plugin/cli.js @@ -50,6 +50,10 @@ const { handler: getPluginVersionHandler, options: getPluginVersionOptions, } = require( './commands/get-plugin-version' ); +const { + handler: getPluginDirHandler, + options: getPluginDirOptions, +} = require( './commands/get-plugin-dir' ); const { handler: enabledModulesHandler, options: enabledModulesOptions, @@ -102,6 +106,11 @@ withOptions( .description( 'Get standalone plugin version' ) .action( catchException( getPluginVersionHandler ) ); +withOptions( program.command( 'get-plugin-dir' ), getPluginDirOptions ) + .alias( 'get-plugin-directory' ) + .description( 'Get plugin directory' ) + .action( catchException( getPluginDirHandler ) ); + withOptions( program.command( 'default-enabled-modules' ), enabledModulesOptions diff --git a/bin/plugin/commands/get-plugin-dir.js b/bin/plugin/commands/get-plugin-dir.js new file mode 100644 index 0000000000..03ca263ac2 --- /dev/null +++ b/bin/plugin/commands/get-plugin-dir.js @@ -0,0 +1,73 @@ +/** + * External dependencies + */ +const path = require( 'path' ); + +/** + * Internal dependencies + */ +const { log } = require( '../lib/logger' ); + +exports.options = [ + { + argname: '-s, --slug ', + description: 'Slug to search out whether it is a plugin or module.', + }, +]; + +/** + * Command to get directory for plugin/module based on the slug. + * + * @param {Object} opt Command options. + * @param {string} opt.slug Plugin/module slug. + */ +exports.handler = async ( opt ) => { + doRunGetPluginDir( { + pluginsJsonFile: 'plugins.json', // Path to plugins.json file. + slug: opt.slug, + } ); +}; + +/** + * Prints directory root for plugin or module based on the slug. + * + * @param {Object} settings Plugin settings. + * @param {string} settings.pluginsJsonFile Path to plugins JSON file. + * @param {string} settings.slug Slug for the plugin or module. + */ +function doRunGetPluginDir( settings ) { + if ( settings.slug === undefined ) { + throw Error( 'A slug must be provided via the --slug (-s) argument.' ); + } + + // Resolve the absolute path to the plugins.json file. + const pluginsFile = path.join( + __dirname, + '../../../' + settings.pluginsJsonFile + ); + + try { + // Read the plugins.json file synchronously. + const { modules, plugins } = require( pluginsFile ); + + for ( const module of Object.values( modules ) ) { + if ( settings.slug === module.slug ) { + log( 'build' ); + return; + } + } + + for ( const plugin of Object.values( plugins ) ) { + if ( settings.slug === plugin.slug ) { + log( 'plugins' ); + return; + } + } + } catch ( error ) { + throw Error( `Error reading file at "${ pluginsFile }": ${ error }` ); + } + + throw Error( + `The "${ settings.slug }" module/plugin slug is missing in the file "${ pluginsFile }".` + ); +} diff --git a/bin/plugin/commands/get-plugin-version.js b/bin/plugin/commands/get-plugin-version.js index cc3fb8c6d2..e9a5649284 100644 --- a/bin/plugin/commands/get-plugin-version.js +++ b/bin/plugin/commands/get-plugin-version.js @@ -19,71 +19,78 @@ exports.options = [ /** * Command to get the plugin version based on the slug. * - * @param {Object} opt Command options. + * @param {Object} opt Command options. + * @param {string} opt.slug Plugin/module slug. */ exports.handler = async ( opt ) => { doRunGetPluginVersion( { pluginsJsonFile: 'plugins.json', // Path to plugins.json file. - slug: opt.slug, // Plugin slug. + slug: opt.slug, } ); }; /** * Returns the match plugin version from plugins.json file. * - * @param {Object} settings Plugin settings. + * @param {Object} settings Plugin settings. + * @param {string} settings.pluginsJsonFile Path to plugins JSON file. + * @param {string} settings.slug Slug for the plugin or module. */ function doRunGetPluginVersion( settings ) { if ( settings.slug === undefined ) { throw Error( 'A slug must be provided via the --slug (-s) argument.' ); } - const pluginsFile = path.join( '.', settings.pluginsJsonFile ); - - // Buffer contents of plugins JSON file. - let pluginsFileContent = ''; + // Resolve the absolute path to the plugins.json file. + const pluginsFile = path.join( + __dirname, + '../../../' + settings.pluginsJsonFile + ); try { - pluginsFileContent = fs.readFileSync( pluginsFile, 'utf-8' ); - } catch ( e ) { - throw Error( `Error reading file at "${ pluginsFile }": ${ e }` ); - } + // Read the plugins.json file synchronously. + const { modules, plugins } = require( pluginsFile ); - // Validate that the plugins JSON file contains content before proceeding. - if ( ! pluginsFileContent ) { - throw Error( - `Contents of file at "${ pluginsFile }" could not be read, or are empty.` - ); - } + for ( const module of Object.values( modules ) ) { + if ( settings.slug === module.slug ) { + log( module.version ); + return; + } + } - const pluginsConfig = JSON.parse( pluginsFileContent ); + for ( const plugin of Object.values( plugins ) ) { + if ( settings.slug === plugin ) { + const readmeFile = path.join( + __dirname, + '../../../plugins/' + plugin + '/readme.txt' + ); - // Check for valid and not empty object resulting from plugins JSON file parse. - if ( - 'object' !== typeof pluginsConfig || - 0 === Object.keys( pluginsConfig ).length - ) { - throw Error( - `File at "${ pluginsFile }" parsed, but detected empty/non valid JSON object.` - ); - } + let fileContent = ''; + try { + fileContent = fs.readFileSync( readmeFile, 'utf-8' ); + } catch ( err ) { + throw Error( + `Error reading the file "${ readmeFile }": "${ err }"` + ); + } - const plugins = pluginsConfig.modules; - if ( ! plugins ) { - throw Error( - `File at "${ pluginsFile }" parsed, but the modules are missing, or they are misspelled.` - ); - } + if ( fileContent === '' ) { + throw Error( `Error reading the file "${ readmeFile }"` ); + } - for ( const moduleDir in plugins ) { - const pluginVersion = plugins[ moduleDir ]?.version; - const pluginSlug = plugins[ moduleDir ]?.slug; - if ( pluginVersion && pluginSlug && settings.slug === pluginSlug ) { - return log( pluginVersion ); + const versionRegex = /(?:Stable tag|v)\s*:\s*(\d+\.\d+\.\d+)/i; + const match = versionRegex.exec( fileContent ); + if ( match ) { + log( match[ 1 ] ); + return; + } + } } + } catch ( error ) { + throw Error( `Error reading file at "${ pluginsFile }": ${ error }` ); } throw Error( - `The "${ settings.slug }" module slug is missing in the file "${ pluginsFile }".` + `The "${ settings.slug }" module/plugin slug is missing in the file "${ pluginsFile }".` ); } diff --git a/bin/plugin/commands/test-plugins.js b/bin/plugin/commands/test-plugins.js index c3d62e3669..6c7a2f42c7 100644 --- a/bin/plugin/commands/test-plugins.js +++ b/bin/plugin/commands/test-plugins.js @@ -2,6 +2,7 @@ * External dependencies */ const fs = require( 'fs-extra' ); +const path = require( 'path' ); const { execSync, spawnSync } = require( 'child_process' ); /** @@ -41,6 +42,7 @@ const { log, formats } = require( '../lib/logger' ); * @property {string} siteType Site type. 'single' or 'multi'. * @property {string} pluginTestAssets Path to 'plugin-tests' folder. * @property {string} builtPluginsDir Path to 'build' directory. + * @property {string} pluginsDir Path to 'plugins' directory. * @property {string} wpEnvFile Path to the plugin tests specific .wp-env.json file. * @property {string} wpEnvDestinationFile Path to the final base .wp-env.json file. * @property {string} performancePluginSlug Slug of the main WPP plugin. @@ -71,6 +73,7 @@ exports.handler = async ( opt ) => { siteType: opt.sitetype || 'single', // Site type. pluginTestAssets: './plugin-tests', // plugin test assets. builtPluginsDir: './build/', // Built plugins directory. + pluginsDir: './plugins/', // Plugins directory. wpEnvFile: './plugin-tests/.wp-env.json', // Base .wp-env.json file for testing plugins. wpEnvDestinationFile: './.wp-env.override.json', // Destination .wp-env.override.json file at root level. wpEnvPluginsRegexPattern: '"plugins": \\[(.*)\\],', // Regex to match plugins string in .wp-env.json. @@ -403,79 +406,69 @@ function doRunStandalonePluginTests( settings ) { // Buffer built plugins array. let builtPlugins = []; - // Buffer contents of plugins JSON file. - let pluginsJsonFileContent = ''; + // Resolve the absolute path to the plugins.json file. + const pluginsFile = path.join( + __dirname, + '../../../' + settings.pluginsJsonFile + ); try { - pluginsJsonFileContent = fs.readFileSync( - settings.pluginsJsonFile, - 'utf-8' - ); - } catch ( e ) { - log( - formats.error( - `Error reading file at "${ settings.pluginsJsonFile }". ${ e }` - ) - ); - } - - // Validate that the plugins JSON file contains content before proceeding. - if ( '' === pluginsJsonFileContent || ! pluginsJsonFileContent ) { - log( - formats.error( - `Contents of file at "${ settings.pluginsJsonFile }" could not be read, or are empty.` - ) - ); - } - - const pluginsConfig = JSON.parse( pluginsJsonFileContent ); - - // Check for valid and not empty object resulting from plugins JSON file parse. - if ( - 'object' !== typeof pluginsConfig || - 0 === Object.keys( pluginsConfig ).length - ) { - log( - formats.error( - `File at "settings.pluginsJsonFile" parsed, but detected empty/non valid JSON object.` - ) - ); - - // Return with exit code 1 to trigger a failure in the test pipeline. - process.exit( 1 ); - } - - const plugins = pluginsConfig.modules; - if ( ! plugins ) { - log( - formats.error( - 'The given module configuration is invalid, the modules are missing, or they are misspelled.' - ) + // Read the plugins.json file synchronously. + const { modules, plugins } = require( pluginsFile ); + + // Create an array of plugins from entries in plugins JSON file. + builtPlugins = Object.keys( modules ) + .filter( ( item ) => { + if ( + ! fs.pathExistsSync( + `${ settings.builtPluginsDir }${ modules[ item ].slug }` + ) + ) { + log( + formats.error( + `Built plugin path "${ settings.builtPluginsDir }${ modules[ item ].slug }" not found, skipping and removing from plugin list` + ) + ); + return false; + } + return true; + } ) + .map( ( item ) => modules[ item ].slug ); + + // Create an array of plugins from entries in plugins JSON file. + builtPlugins = builtPlugins.concat( + Object.values( plugins ) + .filter( ( plugin ) => { + try { + fs.copySync( + `${ settings.pluginsDir }${ plugin.slug }/`, + `${ settings.builtPluginsDir }${ plugin.slug }/`, + { + overwrite: true, + } + ); + log( + formats.success( + `Copied plugin "${ plugin.slug }".\n` + ) + ); + return true; + } catch ( e ) { + // Handle the error appropriately + log( + formats.error( + `Error copying plugin "${ plugin.slug }": ${ e.message }` + ) + ); + return false; + } + } ) + .map( ( plugin ) => plugin.slug ) ); - - // Return with exit code 1 to trigger a failure in the test pipeline. - process.exit( 1 ); + } catch ( error ) { + throw Error( `Error reading file at "${ pluginsFile }": ${ error }` ); } - // Create an array of plugins from entries in plugins JSON file. - builtPlugins = Object.keys( plugins ) - .filter( ( item ) => { - if ( - ! fs.pathExistsSync( - `${ settings.builtPluginsDir }${ plugins[ item ].slug }` - ) - ) { - log( - formats.error( - `Built plugin path "${ settings.builtPluginsDir }${ plugins[ item ].slug }" not found, skipping and removing from plugin list` - ) - ); - return false; - } - return true; - } ) - .map( ( item ) => plugins[ item ].slug ); - // For each built plugin, copy the test assets. builtPlugins.forEach( ( plugin ) => { log(