🚀 WordPress Plugin Deployment
Avoiding the Random Suffix Nightmare
🔥 The Problem
Multiple Plugin Installs with Random Suffixes
When attempting to install or update a WordPress plugin, the system creates multiple copies with random suffixes:
my-plugin/(correct)my-plugin-I94FZu/❌ WRONGmy-plugin-aEWJEy/❌ WRONGmy-plugin-XyZ123/❌ WRONG
This happens repeatedly, creating duplicate installations and breaking plugin functionality.
Why This Matters
- Broken Updates: Auto-update systems fail because WordPress can’t find the correct plugin folder
- Multiple Activations: Users end up with several versions of the same plugin active
- Debugging Nightmare: Difficult to diagnose because the issue appears intermittent
- Production Impact: Can affect hundreds of sites in a plugin distribution network
🔍 Root Cause
The culprit is WordPress’s wp_tempnam() function, which creates temporary files with random suffixes.
How It Happens
- WordPress downloads a plugin ZIP to a temp file using
wp_tempnam($package) - This creates a random filename like
/tmp/my-plugin-aEWJEy.tmp - WordPress uses this temp filename as the basis for the plugin folder name
- Result:
my-plugin-aEWJEy/instead ofmy-plugin/
Where This Occurs
- Auto-update systems using
upgrader_pre_downloadfilter - Plugin installers that don’t specify a fixed temp filename
- Any code that relies on
wp_tempnam()for plugin downloads
✅ The Solution
Use Fixed Temp Filenames
Instead of letting WordPress generate random filenames, specify a fixed temp filename:
❌ WRONG – Creates Random Suffixes
// This creates random suffixes
$temp_file = wp_tempnam($package);
$upgrader = new Plugin_Upgrader();
$result = $upgrader->install($temp_file);
✅ CORRECT – Fixed Filename
// Use a fixed temp filename
$temp_file = sys_get_temp_dir() . '/my-plugin.zip';
file_put_contents($temp_file, $zip_data);
$upgrader = new Plugin_Upgrader();
$result = $upgrader->install($temp_file);
@unlink($temp_file);
Complete Working Example
/**
* Install a WordPress plugin without creating random suffixes
*/
function install_plugin_correctly($download_url, $zip_filename) {
// Download the ZIP file
$response = wp_remote_get($download_url, [
'timeout' => 60
]);
if (is_wp_error($response)) {
return ['success' => false, 'message' => $response->get_error_message()];
}
// Save to temp file with FIXED name (no random suffix)
$temp_file = sys_get_temp_dir() . '/' . $zip_filename;
file_put_contents($temp_file, wp_remote_retrieve_body($response));
// Load WordPress upgrader
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
// Install the plugin - WordPress will create the folder correctly
$upgrader = new Plugin_Upgrader(new WP_Ajax_Upgrader_Skin());
$result = $upgrader->install($temp_file);
// Clean up temp file
@unlink($temp_file);
if (is_wp_error($result)) {
return ['success' => false, 'message' => $result->get_error_message()];
}
return ['success' => true, 'message' => 'Plugin installed successfully'];
}
📦 ZIP File Structure Rules
For Single-File Plugins
✅ CORRECT Structure
my-plugin.zip
└── my-plugin.php (file at root)
WordPress extracts and creates: my-plugin/my-plugin.php
❌ WRONG Structure
my-plugin.zip
└── my-plugin/
└── my-plugin.php (nested folder)
Creates double nesting: my-plugin/my-plugin/my-plugin.php
For Multi-File Plugins
Flat Pack Structure
my-plugin.zip
├── my-plugin.php
├── includes/
│ └── functions.php
├── assets/
│ ├── css/
│ └── js/
└── readme.txt
All files at root of ZIP. WordPress creates the my-plugin/ folder during extraction.
How to Create Correct Zips
PowerShell (Windows)
# For single-file plugin
Compress-Archive -Path my-plugin.php -DestinationPath my-plugin.zip -Force
# For multi-file plugin (from inside the plugin directory)
Compress-Archive -Path * -DestinationPath ../my-plugin.zip -Force
Bash (Linux/Mac)
# For single-file plugin
zip my-plugin.zip my-plugin.php
# For multi-file plugin (from inside the plugin directory)
zip -r ../my-plugin.zip .
🎯 Deployment Best Practices
Rule #1: ONE Plugin Install Per Plugin
NEVER install the same plugin multiple times in a single operation. Each install attempt creates a new folder.
Rule #2: Delete Before Install
ALWAYS follow this sequence:
- Deactivate old version(s)
- Delete ALL old versions (including any with suffixes)
- Wait 0.5 seconds for filesystem to catch up
- Install new version ONCE
- Activate new version
Example: Aggressive Cleanup Before Install
/**
* Aggressively delete all existing plugin folders before installing
*/
function cleanup_before_install($plugin_slug) {
$plugin_dir = WP_PLUGIN_DIR;
// Method 1: Use glob to find all matching folders
$folders = glob($plugin_dir . '/' . $plugin_slug . '*');
if ($folders) {
foreach ($folders as $folder) {
if (is_dir($folder)) {
recursive_delete($folder);
}
}
}
// Method 2: Scan directory for any folders starting with slug
$all_plugins = scandir($plugin_dir);
foreach ($all_plugins as $item) {
if ($item === '.' || $item === '..') continue;
if (strpos($item, $plugin_slug) === 0) {
$full_path = $plugin_dir . '/' . $item;
if (is_dir($full_path)) {
recursive_delete($full_path);
}
}
}
// Wait for filesystem to catch up
usleep(500000); // 0.5 seconds
}
function recursive_delete($dir) {
if (!is_dir($dir)) return;
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
is_dir($path) ? recursive_delete($path) : unlink($path);
}
rmdir($dir);
}
Rule #3: Check What You’re Doing
Before running ANY deployment command:
- Verify the site/environment is correct
- Verify the ZIP URL/path is correct
- Verify you’re not in a loop installing repeatedly
- STOP if you see suffixes appearing – something is wrong
Rule #4: Don’t Retry Failed Methods
If a deployment method creates suffixes:
- STOP immediately
- Don’t try it “one more time”
- Switch to a different method
- Figure out WHY it failed before trying again
🔄 Auto-Update Systems
The Problem with WordPress Auto-Updates
WordPress’s native auto-update system can trigger the random suffix issue if not implemented carefully.
Fixing the upgrader_pre_download Filter
❌ WRONG – Uses wp_tempnam()
add_filter('upgrader_pre_download', function($reply, $package, $upgrader) {
$response = wp_remote_get($package, [
'stream' => true,
'filename' => wp_tempnam($package) // Creates random suffix!
]);
return $response['filename'];
}, 10, 3);
✅ CORRECT – Fixed Filename
add_filter('upgrader_pre_download', function($reply, $package, $upgrader) {
// Extract plugin slug from package URL
$plugin_slug = 'my-plugin'; // Determine from context
// Download to fixed temp filename
$temp_file = sys_get_temp_dir() . '/' . $plugin_slug . '.zip';
$response = wp_remote_get($package, [
'timeout' => 300
]);
if (is_wp_error($response)) {
return $response;
}
file_put_contents($temp_file, wp_remote_retrieve_body($response));
return $temp_file;
}, 10, 3);
Complete Auto-Update Implementation
class My_Plugin_Auto_Updater {
private $plugin_slug = 'my-plugin';
private $github_repo = 'username/my-plugin';
public function __construct() {
add_filter('pre_set_site_transient_update_plugins', [$this, 'check_for_updates']);
add_filter('plugins_api', [$this, 'plugin_info'], 10, 3);
add_filter('upgrader_pre_download', [$this, 'download_with_auth'], 10, 3);
}
public function check_for_updates($transient) {
if (empty($transient->checked)) {
return $transient;
}
// Fetch latest release from GitHub
$response = wp_remote_get(
"https://api.github.com/repos/{$this->github_repo}/releases/latest"
);
if (is_wp_error($response)) {
return $transient;
}
$release = json_decode(wp_remote_retrieve_body($response), true);
$new_version = ltrim($release['tag_name'], 'v');
// Get current version
$plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $this->plugin_slug . '.php');
$current_version = $plugin_data['Version'];
// If new version available, add to transient
if (version_compare($new_version, $current_version, '>')) {
$plugin_file = $this->plugin_slug . '/' . $this->plugin_slug . '.php';
$transient->response[$plugin_file] = (object) [
'slug' => $this->plugin_slug,
'new_version' => $new_version,
'package' => $release['assets'][0]['url'], // GitHub API URL
'url' => $release['html_url']
];
}
return $transient;
}
public function download_with_auth($reply, $package, $upgrader) {
// Only intercept GitHub API URLs
if (strpos($package, 'api.github.com') === false) {
return $reply;
}
// Download with authentication
$response = wp_remote_get($package, [
'headers' => [
'Accept' => 'application/octet-stream',
'User-Agent' => 'WordPress-Plugin-Updater'
],
'timeout' => 300
]);
if (is_wp_error($response)) {
return $response;
}
// Save to FIXED temp filename (prevents random suffixes)
$temp_file = sys_get_temp_dir() . '/' . $this->plugin_slug . '.zip';
file_put_contents($temp_file, wp_remote_retrieve_body($response));
return $temp_file;
}
}
✓ Pre-Deployment Checklist
Before deploying ANY plugin update to production sites:
- Verify ZIP structure is correct (file at root for single-file plugins)
- Test installation on ONE site first
- Verify NO suffixes are created
- Verify old version is properly replaced
- Check that activation works
- Only then proceed to mass deployment
- Monitor first 10 sites for any issues
- If ANY suffixes appear, STOP IMMEDIATELY
📋 Quick Reference Table
| Problem | Cause | Solution |
|---|---|---|
| Random suffixes on plugin folders | wp_tempnam() creates random filenames |
Use sys_get_temp_dir() . '/' . $zip_file |
| Double nested folders | ZIP contains folder/folder/file | ZIP should contain file at root |
| Multiple plugin installs | Installing repeatedly without deleting old versions | Delete old versions first, install once |
| Auto-update creating suffixes | upgrader_pre_download using wp_tempnam() |
Use fixed temp filename in download handler |
🎓 Conclusion
The random suffix problem in WordPress plugin deployment is a subtle but devastating issue that can cost days of debugging time. The root cause is always the same: wp_tempnam() creating random filenames that WordPress uses as the basis for plugin folder names.
The Golden Rule
Always use fixed temp filenames for plugin installations:
$temp_file = sys_get_temp_dir() . '/' . $plugin_slug . '.zip';
This single line of code can save you days of frustration.
Remember:
- Delete old versions before installing new ones
- Use correct ZIP structure (file at root for single-file plugins)
- Test on one site before mass deployment
- Stop immediately if you see random suffixes appearing
By following these guidelines, you can avoid the random suffix nightmare and deploy WordPress plugins reliably and efficiently.
