Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement publishing workflow for standalone plugins that aren't modules #1000

Merged
merged 12 commits into from
Feb 27, 2024
34 changes: 28 additions & 6 deletions .github/workflows/deploy-standalone-plugins.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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' }}
Expand All @@ -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
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
else
echo "The ${{ inputs.slug }} module slug is missing in the file plugins.json."
exit 1
Expand All @@ -50,7 +67,7 @@ jobs:
# for use in the matrix.
# 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 | to_entries[] | {name:.key,slug:.value.slug,version:.value.version,directory:"plugins","dry-run":true }])}' plugins.json)" >> $GITHUB_OUTPUT
fi
deploy:
name: Deploy Plugin
Expand All @@ -69,15 +86,20 @@ jobs:
- name: Install npm dependencies
run: npm ci
- name: Building standalone plugins
if: ${{ matrix.directory != 'plugins' }}
run: npm run build-plugins
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
- 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
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
SLUG: ${{ matrix.slug }}
VERSION: ${{ matrix.version }}
BUILD_DIR: ./build/${{ matrix.slug }}
ASSETS_DIR: ./build/${{ matrix.slug }}/.wordpress-org
BUILD_DIR: ./${{ matrix.directory }}/${{ matrix.slug }}
ASSETS_DIR: ./${{ matrix.directory }}/${{ matrix.slug }}/.wordpress-org
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 9 additions & 0 deletions bin/plugin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
96 changes: 96 additions & 0 deletions bin/plugin/commands/get-plugin-dir.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* External dependencies
*/
const fs = require( 'fs' );
const path = require( 'path' );

/**
* Internal dependencies
*/
const { log } = require( '../lib/logger' );

exports.options = [
{
argname: '-s, --slug <slug>',
description: 'Plugin or Standalone module/plugin slug to get directory',
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
},
];

/**
* Command to get directory for plugin/module based on the slug.
*
* @param {Object} opt Command options.
*/
exports.handler = async ( opt ) => {
doRunGetPluginDir( {
pluginsJsonFile: 'plugins.json', // Path to plugins.json file.
slug: opt.slug, // Plugin slug.
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
} );
};

/**
* Returns directory for plugin or module based on the slug.
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
*
* @param {Object} settings Plugin settings.
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
*/
function doRunGetPluginDir( 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 = '';

try {
pluginsFileContent = fs.readFileSync( pluginsFile, 'utf-8' );
} catch ( e ) {
throw Error( `Error reading file at "${ pluginsFile }": ${ e }` );
}

// 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.`
);
}

const pluginsConfig = JSON.parse( pluginsFileContent );

// 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.`
);
}
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved

const stPlugins = pluginsConfig.modules;
if ( stPlugins ) {
for ( const moduleDir in stPlugins ) {
const pluginVersion = stPlugins[ moduleDir ]?.version;
const pluginSlug = stPlugins[ moduleDir ]?.slug;
if ( pluginVersion && pluginSlug && settings.slug === pluginSlug ) {
return log( 'build' );
}
}
}

const plugins = pluginsConfig.plugins;
if ( plugins ) {
for ( const pluginDir in plugins ) {
const pluginVersion = plugins[ pluginDir ]?.version;
const pluginSlug = plugins[ pluginDir ]?.slug;
if ( pluginVersion && pluginSlug && settings.slug === pluginSlug ) {
return log( 'plugins' );
}
}
}

throw Error(
`The "${ settings.slug }" module/plugin slug is missing in the file "${ pluginsFile }".`
);
}
29 changes: 18 additions & 11 deletions bin/plugin/commands/get-plugin-version.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,29 @@ function doRunGetPluginVersion( settings ) {
);
}

const plugins = pluginsConfig.modules;
if ( ! plugins ) {
throw Error(
`File at "${ pluginsFile }" parsed, but the modules are missing, or they are misspelled.`
);
const stPlugins = pluginsConfig.modules;
if ( stPlugins ) {
for ( const moduleDir in stPlugins ) {
const pluginVersion = stPlugins[ moduleDir ]?.version;
const pluginSlug = stPlugins[ moduleDir ]?.slug;
if ( pluginVersion && pluginSlug && settings.slug === pluginSlug ) {
return log( pluginVersion );
}
}
}

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 plugins = pluginsConfig.plugins;
if ( plugins ) {
for ( const moduleDir in plugins ) {
const pluginVersion = plugins[ moduleDir ]?.version;
const pluginSlug = plugins[ moduleDir ]?.slug;
if ( pluginVersion && pluginSlug && settings.slug === pluginSlug ) {
return log( pluginVersion );
}
}
}

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 }".`
);
}
58 changes: 52 additions & 6 deletions bin/plugin/commands/test-plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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.
Expand Down Expand Up @@ -71,6 +72,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.
Expand Down Expand Up @@ -445,8 +447,8 @@ function doRunStandalonePluginTests( settings ) {
process.exit( 1 );
}

const plugins = pluginsConfig.modules;
if ( ! plugins ) {
const stPlugins = pluginsConfig.modules;
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
if ( ! stPlugins ) {
log(
formats.error(
'The given module configuration is invalid, the modules are missing, or they are misspelled.'
Expand All @@ -458,23 +460,67 @@ function doRunStandalonePluginTests( settings ) {
}

// Create an array of plugins from entries in plugins JSON file.
builtPlugins = Object.keys( plugins )
builtPlugins = Object.keys( stPlugins )
.filter( ( item ) => {
if (
! fs.pathExistsSync(
`${ settings.builtPluginsDir }${ plugins[ item ].slug }`
`${ settings.builtPluginsDir }${ stPlugins[ item ].slug }`
)
) {
log(
formats.error(
`Built plugin path "${ settings.builtPluginsDir }${ plugins[ item ].slug }" not found, skipping and removing from plugin list`
`Built plugin path "${ settings.builtPluginsDir }${ stPlugins[ item ].slug }" not found, skipping and removing from plugin list`
)
);
return false;
}
return true;
} )
.map( ( item ) => plugins[ item ].slug );
.map( ( item ) => stPlugins[ item ].slug );

// Append plugins into array.
const plugins = pluginsConfig.plugins;
if ( ! plugins ) {
log(
formats.error(
'The given plugin configuration is invalid, the plugins are missing, or they are misspelled.'
)
);

// Return with exit code 1 to trigger a failure in the test pipeline.
process.exit( 1 );
}
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved

// Create an array of plugins from entries in plugins JSON file.
builtPlugins = builtPlugins.concat(
Object.keys( plugins )
.filter( ( item ) => {
try {
fs.copySync(
`${ settings.pluginsDir }${ plugins[ item ].slug }/`,
`${ settings.builtPluginsDir }${ plugins[ item ].slug }/`,
{
overwrite: true,
}
);
log(
formats.success(
`Copied plugin "${ plugins[ item ].slug }".\n`
)
);
return true;
} catch ( e ) {
// Handle the error appropriately
log(
formats.error(
`Error copying plugin "${ plugins[ item ].slug }": ${ e.message }`
)
);
return false;
}
} )
.map( ( item ) => plugins[ item ].slug )
mukeshpanchal27 marked this conversation as resolved.
Show resolved Hide resolved
);

// For each built plugin, copy the test assets.
builtPlugins.forEach( ( plugin ) => {
Expand Down
7 changes: 6 additions & 1 deletion plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@
"version": "1.0.5"
}
},
"plugins": [ "auto-sizes" ]
"plugins": {
"auto-sizes": {
"slug": "auto-sizes",
"version": "1.0.1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be necessary if the version is already present here:

Should this not rather parse the version out of the plugin file? Or we also have it present in the readme.txt:

I think we should avoid having this version in three different places.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we just store the version in Stable Tag of readme.txt then the plugin file could have n.e.x.t as the version which gets replaced during deployment, or vice-versa.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or the version could be stored in plugins.json like you have here, but if so, then n.e.x.t should be the version in auto-sizes.php and readme.txt to avoid having to manually keep them in sync.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the idea of storing the version in a single place only. As mentioned n.e.x.t strategy can be used to populate the version on the fly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @westonruter, for bringing this up. The plugin versions were added for the deploy process per #935 (comment). Since there is no build project for plugins, in my opinion, we should discuss this first and address it in a follow-up issue. What do you think? cc. @joemcgill

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I saw that comment. Still, not ideal to have the version in triplicate. If you want to address that in another issue, sure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open a separate issue for further work: #1006

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I spent some time looking into how we could modify the release job to avoid the duplication of version numbers here for this, and I'm not sure it's worth the effort for now. Once we finish migrating all of the standalone modules to the plugins folder we can clean this up more easily.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mukeshpanchal27 @joemcgill I just voiced a similar concern as @westonruter in #935 (comment), and his feedback is precisely why the requirements only mentioned plugin slugs (no need to duplicate the version number).

I'm okay for this to be addressed in a separate issue, but since this PR is actually making the change and the feature/modules-to-plugins branch currently uses the correct format, it's a little strange IMO to go ahead with this change just to change it back afterwards.

If this PR is blocking other work and therefore would benefit from a merge ASAP, I'm okay to fix this again in a follow up issue, but it should be addressed shortly after this PR, in time for 3.0, as it requires changes to the plugins.json file format/spec, which will affect documentation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback.

In a4cf3bf, I have reverted the version-related changes for plugins and instead fetched the version from the readme.txt file. I updated the script accordingly. I opted not to use the plugin's main file because different plugins have different main files, such as plugin/load.php or plugin/plugin.php.

}
}
}
Loading