Skill: WordPress Plugin Deployment – Avoiding the Random Suffix Nightmare

WordPress Plugin Deployment: Avoiding the Random Suffix Nightmare

🚀 WordPress Plugin Deployment

Avoiding the Random Suffix Nightmare

⚠️ Critical Knowledge This guide documents a critical WordPress plugin deployment issue that cost 2+ days of debugging across multiple incidents. If you’re deploying custom WordPress plugins, especially single-file plugins or auto-update systems, this information could save you days of frustration.

🔥 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/ ❌ WRONG
  • my-plugin-aEWJEy/ ❌ WRONG
  • my-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

  1. WordPress downloads a plugin ZIP to a temp file using wp_tempnam($package)
  2. This creates a random filename like /tmp/my-plugin-aEWJEy.tmp
  3. WordPress uses this temp filename as the basis for the plugin folder name
  4. Result: my-plugin-aEWJEy/ instead of my-plugin/

Where This Occurs

  • Auto-update systems using upgrader_pre_download filter
  • 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:

  1. Deactivate old version(s)
  2. Delete ALL old versions (including any with suffixes)
  3. Wait 0.5 seconds for filesystem to catch up
  4. Install new version ONCE
  5. 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:

  1. STOP immediately
  2. Don’t try it “one more time”
  3. Switch to a different method
  4. 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

🏷️ Tags

WordPress Plugin Development Deployment Auto-Update PHP Debugging Best Practices wp_tempnam Plugin Upgrader

🎓 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.

WordPress Plugin Deployment Guide

Published: November 2025

Lessons learned from 2+ days of debugging across multiple production incidents