Menu Search
Jump to the content X X

Today, too many websites are still inaccessible. In our new book Inclusive Design Patterns, we explore how to craft flexible front-end design patterns and make future-proof and accessible interfaces without extra effort. Hardcover, 312 pages. Get the book now →

How To Deploy WordPress Plugins With GitHub Using Transients

If you’ve worked with WordPress for a while, you may have tried your hand at writing a plugin. Many developers will start creating plugins to enhance a custom theme or to modularize their code. Eventually, though, you may want to distribute your plugin to a wider audience.

While you always have the option to use the WordPress Subversion repository, there may be instances where you prefer to host a plugin yourself. Perhaps you are offering your users a premium plugin. Maybe you need a way to keep your client’s code in sync across multiple sites. It could simply be that you want to use a Git workflow instead of Subversion. Whatever the reason, this tutorial will show you how to set up a GitHub repository to push updates to your plugin, wherever it resides.

The Plan Link

Before we get too far into the code we should start with an outline of what we will be doing:

  1. First, we will learn a little bit about transients and how they work within WordPress.
  2. Then, we will build a PHP class to house all of our code.
  3. Next, we are going to connect our plugin to GitHub.
  4. Lastly, we will create the interface elements that allow users to interact with our plugin.

When you finish this article you should have a fully functioning plugin that will update directly using GitHub releases. Ready to get started?

WordPress Transients Link

First of all, what are WordPress transients1? WordPress transients are a short-lived entry of data that is stored for a defined duration and will be automatically removed when it has expired — think of them as server-side cookies. WordPress uses transients to store information about all of the plugins that have been installed, including their version number. Every once in a while WordPress will refresh the data that is stored in the transient. It is this event that WordPress uses to check the Subversion repository for updated plugins. It is also this event that we will use to check for updates on our own plugin on GitHub.

Getting Started Link

Let’s begin by setting up our updater class:

class Smashing_Updater {
  protected $file;
  
  public function __construct( $file ) {
    $this->file = $file;
    return $this;
  }
}

The first thing we must do is create a class name and a constructor (the class name can be anything you want). We are going to need to figure out some basic information about the WordPress plugin we are updating, including the version number. We’ll do this by passing the main plugin file’s path into our updater and then we’ll assign that value to a property.

You might be wondering why we need to pass the plugin’s path into our class. You see, the way WordPress stores plugin information is by using the main plugin file’s path as a unique identifier (i.e. the basename). Let’s say our plugin is in the directory /wp-content/plugins/smashing-plugin/smashing-plugin.php, the basename for our plugin would be smashing-plugin/smashing-plugin.php. We will use this basename to check if the plugin we are updating is activated, among other things.

Next we need to get the plugin data and set it to a property in our class:

class Smashing_Updater {
  protected $file;
  protected $plugin;
  protected $basename;
  protected $active;
  
  public function __construct( $file ) {
    $this->file = $file;
    add_action( 'admin_init', array( $this, 'set_plugin_properties' ) );
    return $this;
  }
  
  public function set_plugin_properties() {
    $this->plugin   = get_plugin_data( $this->file );
    $this->basename = plugin_basename( $this->file );
    $this->active   = is_plugin_active( $this->basename );
  }
}

You may have noticed that I am using the action admin_init2 to set the plugin properties. This is because the function get_plugin_data() may not have been defined at the point in which this code was called. By hooking it to admin_init we are ensuring that we have that function available to get our plugin data. We are also checking if the plugin is activated, and assigning that and the plugin object to properties in our class.

To learn more about WordPress actions and filters you should take a look at the plugin API3.

Now that we have our class set up and grabbed some of the basic plugin data we’ll need, it’s time we start talking about GitHub.

Setting Up GitHub Link

We can start by creating a repository for our plugin. It is important to remember that we will pull the files down from here, so the directory structure is important. You’ll need the root of your repository to be the contents of your plugin folder, not the plugin folder itself. Here is an example of what you should see:

The root directory of our GitHub repository4
The root directory of our GitHub repository. (View large version5)

In my repository I have two files. The main plugin file smashing-plugin.php and the updater script updater.php. Your repo may look a little different depending on your plugin files.

Now that we have a repository set up, let’s talk about how we are going to check the version number on GitHub. We have a couple of options here. We could pull down the main file and parse the contents to try to find the version number. However, I prefer using the built-in release functionality of GitHub. Let’s add a new release.

Start by going to the “releases” tab and hitting “Create a new release”:

The releases tab of our GitHub repository
Creating a new release in the GitHub release section.

Here you will find a few fields we need to fill out. The most important one is the “Tag version” field. This is where we will put the current release version of our plugin. By now, you are probably familiar with the semantic versioning format6. This is the format we are going to use for our field. We should also use this format for our main plugin file, in the PHP comments. Continue filling out the title and description fields in the release until your release looks like something like this:

A new release of our plugin7
A new release of our plugin. (View large version8)

Hit the “Publish release” button to create the release. This will create a ZIP file in our repo that we can use in our updater script. Then, you simply repeat this process whenever you want to deploy a new plugin release. We can now use the GitHub API to check the version number and we have a ZIP file ready to go.

But I Have A Private Plugin! Link

Don’t worry, just a couple of extra steps. Private plugins require you to pass an authorization token when you make requests to the API. First go to your account settings:

GitHub settings panel
GitHub settings panel.

Then go to “Personal access tokens” on the left menu and click “Generate a new access token” button on the top right. You will be brought to this page:

GitHub generate token9
GitHub generate token. (View large version10)

You can give the token any description you like, then select the repo scope and hit “Generate token”. Once you’ve generated your token, copy it down somewhere — we will use it later.

Connecting To GitHub Link

We can now start adding some code to connect our updater to GitHub. First we’ll add a few more properties to hold our repo information, and one to hold our response from GitHub:

private $username;
private $repository;
private $authorize_token;
private $github_response;

Then we’ll add some setters for our new properties:

public function set_username( $username ) {
  $this->username = $username;
}
public function set_repository( $repository ) {
  $this->repository = $repository;
}
public function authorize( $token ) {
  $this->authorize_token = $token;
}

These setters allow us to modify usernames and repositories without reinstantiating our updater class. Say you wanted to have an unlockable feature or require the users to register, you could change the repo to a pro version of your plugin by simply adding a conditional statement. Let’s take a look at how we should include this code in our main plugin file:

// Include our updater file
include_once( plugin_dir_path( __FILE__ ) . 'update.php');

$updater = new Smashing_Updater( __FILE__ ); // instantiate our class
$updater->set_username( 'rayman813' ); // set username
$updater->set_repository( 'smashing-updater-plugin' ); // set repo

Now let’s write the code we will need to get the version tag from GitHub.

private function get_repository_info() {
  if ( is_null( $this->github_response ) ) { // Do we have a response?
    $request_uri = sprintf( 'https://api.github.com/repos/%s/%s/releases', $this->username, $this->repository ); // Build URI
    if( $this->authorize_token ) { // Is there an access token?
        $request_uri = add_query_arg( 'access_token', $this->authorize_token, $request_uri ); // Append it
    }        
    $response = json_decode( wp_remote_retrieve_body( wp_remote_get( $request_uri ) ), true ); // Get JSON and parse it
    if( is_array( $response ) ) { // If it is an array
        $response = current( $response ); // Get the first item
    }
    if( $this->authorize_token ) { // Is there an access token?
        $response['zipball_url'] = add_query_arg( 'access_token', $this->authorize_token, $response['zipball_url'] ); // Update our zip url with token
    }
    $this->github_response = $response; // Set it to our property  
  }
}

In this method we are checking if we’ve already gotten a response; if we haven’t, then make a request to the GitHub API endpoint using the username and repo that we’ve supplied. We’ve also added some code to check for an access token for private repos. If there is one, append it to the URL and update the zipball URL (the file we will download when we update). We are then getting the JSON response, parsing it, and grabbing the latest release. Once we have the latest release, we’re setting it to the property we created earlier.

Modifying WordPress Link

Alright, so far we’ve collected information about our plugin and our repo, and we’ve connected to the repo using the API. Now it’s time to start modifying WordPress to find our new data and put it in the right spots. This is going to involve three steps:

  1. Modifying the output of the WordPress update transient.
  2. Updating the WordPress interface to show our plugin’s info properly.
  3. Making sure our plugin is working properly after the update.

Before we get too deep into how we are going to accomplish each step, we should talk about how we are going to hook into the transient.

Hooking Into The Update Transient Link

There are a few ways to hook into this transient event. There are two filters and two actions we can use to inject our code; they are:

  • Filter: pre_set_transient_update_plugins
  • Filter: pre_set_site_transient_update_plugins
  • Action: set_transient_update_plugins
  • Action: set_site_transient_update_plugins

You will notice that the tags are fairly similar. The difference here is the addition of the word site. This distinction is important because it changes the scope of our injected code. The tags without the word site will only work on single-site installations of WordPress; the other will work on both single-site and multi-site installations. Depending on your plugin you may choose to use one over the other.

I simply want to modify the default transient, so I am going to use one of the filters. I’ve decided to use pre_set_site_transient_update_plugins so that our code will work on both single and multi-site installations.

Update: If you are using this script to update a theme you can also use the filter pre_set_site_transient_update_themes. Source: Andy Fragen11

Let’s take a look at our initialize function:

public function initialize() {
  add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'modify_transient' ), 10, 1 );
  add_filter( 'plugins_api', array( $this, 'plugin_popup' ), 10, 3);
  add_filter( 'upgrader_post_install', array( $this, 'after_install' ), 10, 3 );
}

In this example I have added three filters. Each one corresponds to the steps we talked about earlier. Our first filter is going to modify the transient; the second will ensure that our plugin data gets passed into the WordPress interface; and the last will make sure that our plugin is activated after the update.

Now we need to call this new method in our main plugin file to initialize the updater script. Here is the what the updated code should look like:

// Include our updater file
include_once( plugin_dir_path( __FILE__ ) . 'update.php');

$updater = new Smashing_Updater( __FILE__ ); // instantiate our class
$updater->set_username( 'rayman813' ); // set username
$updater->set_repository( 'smashing-plugin' ); // set repo
$updater->initialize(); // initialize the updater

Now our initialize method will run and the filters within the method will be active. If you are planning on having your plugin’s automatic updates be a premium feature, the initialize method would be a good spot to put your checks and conditionals.

Writing The Code For Our Filters Link

I have written out all of the methods we will need for each filter we will be using. We can start with the modify_transient method:

public function modify_transient( $transient ) {

  if( property_exists( $transient, 'checked') ) { // Check if transient has a checked property
    if( $checked = $transient->checked ) { // Did WordPress check for updates?
      $this->get_repository_info(); // Get the repo info
      $out_of_date = version_compare( $this->github_response['tag_name'], $checked[$this->basename], 'gt' ); // Check if we're out of date
      if( $out_of_date ) {
        $new_files = $this->github_response['zipball_url']; // Get the ZIP
        $slug = current( explode('/', $this->basename ) ); // Create valid slug
        $plugin = array( // setup our plugin info
          'url' => $this->plugin["PluginURI"],
          'slug' => $slug,
          'package' => $new_files,
          'new_version' => $this->github_response['tag_name']
        );
        $transient->response[ $this->basename ] = (object) $plugin; // Return it in response
      }
    }
  }
  return $transient; // Return filtered transient
}

This snippet simply takes the version number from the comments in our main plugin file, and compares them with the tag name we gave our release on GitHub. If the one on GitHub is higher, our code tells WordPress that there is a newer version available. If you change the version number in the main plugin file to a version that is lower than our GitHub tag, you should start seeing the update notification in the WordPress admin:

WordPress plugin interface showing updates available12
WordPress plugin interface showing updates available. (View large version13)

You might notice, however, that if you click on the “View version 1.0.0 details” link, you receive an error from WordPress. This is because WordPress is trying to find the details of this plugin from its repositories. Instead, we need to load our own data about the plugin. That is where our next method, plugin_popup, comes in:

public function plugin_popup( $result, $action, $args ) {
  if( ! empty( $args->slug ) ) { // If there is a slug
    if( $args->slug == current( explode( '/' , $this->basename ) ) ) { // And it's our slug
      $this->get_repository_info(); // Get our repo info
      // Set it to an array
      $plugin = array(
        'name'              => $this->plugin["Name"],
        'slug'              => $this->basename,
        'version'           => $this->github_response['tag_name'],
        'author'            => $this->plugin["AuthorName"],
        'author_profile'    => $this->plugin["AuthorURI"],
        'last_updated'      => $this->github_response['published_at'],
        'homepage'          => $this->plugin["PluginURI"],
        'short_description' => $this->plugin["Description"],
        'sections'          => array( 
            'Description'   => $this->plugin["Description"],
            'Updates'       => $this->github_response['body'],
        ),
        'download_link'     => $this->github_response['zipball_url']
      );
      return (object) $plugin; // Return the data
    }
  }   
  return $result; // Otherwise return default
}

This snippet is actually pretty simple. We are just checking if WordPress is looking for data about our plugin, and if it is, returning our own array of data. We get the data from a combination of our GitHub response and our plugin’s comment data. You might have also noticed the sections key in this array. Each item in that array is output as a tab in the plugin popup meta box. You could add pretty much anything you’d like in those sections, including HTML. Right now, we are just showing the plugin description and the release notes we wrote earlier.

Plugin update popup14
Plugin update popup. (View large version15)

Let’s take a look at our final method, after_install:

public function after_install( $response, $hook_extra, $result ) {
  global $wp_filesystem; // Get global FS object

  $install_directory = plugin_dir_path( $this->file ); // Our plugin directory 
  $wp_filesystem->move( $result['destination'], $install_directory ); // Move files to the plugin dir
  $result['destination'] = $install_directory; // Set the destination for the rest of the stack
   
  if ( $this->active ) { // If it was active
    activate_plugin( $this->basename ); // Reactivate
  }
  return $result;
}

This snippet does two things. First, it sets an argument named destination, which is simply the directory where WordPress is going to install our new code. We are passing the main plugin file’s directory into this argument since it should always be the root of the plugin. Second, it checks if the plugin was activated using the property we set earlier. If it was active before updating, our method will reactivate it.

Conclusion Link

Congratulations! You should now be able to update your plugin by clicking the “Update now” button on the plugins page. You should keep in mind, though, that your plugin’s version in the main plugin file needs to be updated to match the current release of your plugin — they should always match. If you didn’t update your version in the comments, you may find yourself in an endless “There is an update available” loop.

If you would like to see the completed script from this tutorial, you can view it in the sample GitHub repository16 that we made.

(dp, ml, og)

Footnotes Link

  1. 1 https://codex.wordpress.org/Transients_API
  2. 2 https://codex.wordpress.org/Plugin_API/Action_Reference
  3. 3 https://codex.wordpress.org/Plugin_API
  4. 4 https://www.smashingmagazine.com/wp-content/uploads/2015/07/01-github_root-opt.png
  5. 5 https://www.smashingmagazine.com/wp-content/uploads/2015/07/01-github_root-opt.png
  6. 6 http://semver.org/
  7. 7 https://www.smashingmagazine.com/wp-content/uploads/2015/07/03-new_release-opt.png
  8. 8 https://www.smashingmagazine.com/wp-content/uploads/2015/07/03-new_release-opt.png
  9. 9 https://www.smashingmagazine.com/wp-content/uploads/2015/07/05-token-opt.png
  10. 10 https://www.smashingmagazine.com/wp-content/uploads/2015/07/05-token-opt.png
  11. 11 https://github.com/afragen
  12. 12 https://www.smashingmagazine.com/wp-content/uploads/2015/07/06-updates_available-opt.png
  13. 13 https://www.smashingmagazine.com/wp-content/uploads/2015/07/06-updates_available-opt.png
  14. 14 https://www.smashingmagazine.com/wp-content/uploads/2015/07/07-update_popup-opt.png
  15. 15 https://www.smashingmagazine.com/wp-content/uploads/2015/07/07-update_popup-opt.png
  16. 16 https://github.com/rayman813/smashing-plugin

↑ Back to top Tweet itShare on Facebook

Advertisement

Matthew Ray is a full-stack web developer working for the worldwide public relations firm, Weber Shandwick. He currently focusses his efforts on product development, plugin architecture, and service integration. In his free time, Matthew is an avid photographer, musician and bulldog enthusiast.

  1. 1

    Mark F. Simchock

    August 11, 2015 4:01 pm

    Nice!

    Any chance of doing this with BitBucket? Specifically their private repos?

    7
    • 2

      Thanks! I would imagine that the Bit Bucket API would allow you to do the same thing. It would take some refactoring in the API section, but the important parts would just be getting the latest release zip file and its version number. The rest could be done on the script’s side.

      As for private repos, I think you could add an OAuth consumer on the BitBucket side to skip the OAuth handshake and essentially have the script update in the same way.

      If enough people are interested in this, I may take a deeper dive and give more details!

      25
    • 4

      Second that.

      0
  2. 5

    Pascal Birchler

    August 11, 2015 5:28 pm

    It’s worth mentioning that there’s an existing GitHub Updater for WordPress Plugins that works for Bitbucket, GitHub and Gitlab and has been around for about 2 years now.

    https://github.com/afragen/github-updater

    4
    • 6

      Thanks! I agree this plugin is definitely worth mentioning and I have actually used it in the past – and it’s great!

      The only issue I had with this plugin was that it needed to be distributed along with your plugin to work properly. Andy Fragen, the author, suggests installing it as a Must-Use plugin alongside your plugins/themes. This works if you are simply managing client sites or personal sites and want to keep them in sync since you have control over the installed plugins. However, if you wish to distribute the plugin to an unknown audience, they also need to install the GitHub Updater plugin. What’s more is that private repos require the end user to input their access token directly in the admin. Again, great for sites you own, but not for distribution.

      The great part about this script is that, for the end user anyway, the update is completely contained within the developer’s custom plugin. The end user is completely unaware that the update is not coming from the SVN server and they also don’t need to input any data.

      3
      • 7

        Hey Matthew!

        The latest develop branch, and soon master, will have a filter that allows a developer to pass a key/value pair as the repo => access-token so the end user doesn’t have to fill in that data. There will also be a filter to hide the entire Settings page if you really don’t want you client to access it.

        As for requiring the first installation of GHU, yes it’s required. But the concept is that extra non-relevant code doesn’t need to be added to your project. Plus the end user benefits from any updates to the Updater code.

        1
        • 8

          I hope you don’t think I am knocking GHU, I think it’s awesome. I actually wrote this script because I was using GHU but I needed a way to send updates to sites that didn’t have GHU installed. Great work man!

          1
          • 9

            Matthew,

            I thought no such thing. Great article and keep up the great work. I wish there were more articles like this when I started.

            1
      • 10

        In my opinion, putting the update inside the plugin is only helpful if you develop 1 plugin. What happening if we develop 10 plugins? Copy the same code to 10 repos and the worst thing is if the updater code is changed, we have to redo the copy job 10 times.

        0
        • 11

          True, adding it to multiple places would be a pain. However, you could create a Git submodule with the updater script and simply move the local repo information into the individual plugins. That way your updates trickle down into all of your plugins.

          1
    • 12

      Thanks for mentioning GitHub Updater Pascal.

      It also works with public or private repos and allows for better remote installation of plugins or themes hosted on any at the above git systems.

      2
  3. 13

    Mark F. Simchock

    August 11, 2015 6:13 pm

    One more question, please :)

    re: The paragraph starting with “You might be wondering why we need to pass the plugin’s path into our class…”

    Is it possible to trick WP into also installing to the mu-plugins folder? Or better still a specified subfolder of mu-plugins?

    Or even to the themes folder for that matter?

    0
    • 14

      I looked into this for you to see if you can use it for Must-Use plugins and found the following line in the WordPress Codex under the Caveats section:

      “Plugins in the must-use directory will not appear in the update notifications nor show their update status on the plugins page, so you are responsible for learning about and performing updates on your own.”

      So I don’t think it will work for that use, however I believe this code should work properly for updating themes, but I would have to check to make sure the filters I am using are correct. There may be a different one for themes. I will dig a little more into this and report back here with my findings! :)

      0
      • 15

        Mark F. Simchock

        August 11, 2015 10:08 pm

        Yeah. I was afraid you were gonna say that.

        That said, there’s got to be some sort of work around for this. Wouldn’t ya think?

        0
        • 16

          The only way to update a mu-plugin is to have a symlink of the plugin in the mu-plugins folder and have the plugin in the regular plugins folder. Then updating the plugin will update the my-plugin.

          You can see how I do this with GitHub Updater in the README.

          0
        • 17

          If you are managing your own MU plugins on github for your own site you can set up Git auto pull https://github.com/WolfieZero/github-auto-pull

          You’ll need to jump through a few hoops like using SSH, setting up a deploy key, and it’s best with a private repo.

          0
      • 18

        There are different filters for themes, but main one your looking for is pre_set_site_transient_update_themes.

        1
  4. 20

    Thank’s Crucial article finally i’m understand something with ‘transient’.

    3
  5. 21

    Shawn Beelman

    August 12, 2015 9:40 pm

    Hey Matthew, thanks for this tutorial. I have a plugin for which I’ve been trying to add a Github updater just like this.

    I just added this as an issue in your repo, but I wanted to post it here as well – I’m getting an ‘undefined index’ notice for line 79 in updater.php:

    if( $checked = $transient->checked )

    I’m also wondering if that shouldn’t be a “==” instead of “=”.

    Not sure if this is related, but I’m getting intermittent results when testing a new release. Sometimes WP is seeing, sometimes not. When it does, the “update now” link isn’t showing on the plugins page, I have to go to the updates page to apply it.

    0
    • 22

      Hey there, I’m taking a look at the issue you posted on GitHub to resolve the checked property, that should always be defined in that filter, so I may just add a check to see if it’s defined before executing the rest of the code. As for the “=” vs “==”, I am not actually doing a comparison in that conditional, I’m checking if the $checked variable is set to a non-false value, so the single equal sign is correct in this case.

      I will add my additional comments to the Git issues section and take a look at your update link issue there as well.

      0
  6. 23

    Hey Matthew!

    Providing plugin updates directly through Github seems to be a really nice way to do this so thanks for sharing!

    I recently tried to make it work but for some reason, clicking on the ‘update now’ button doesn’t perform an action at all and causes the following JS error instead:

    Error: Syntax error, unrecognized expression: #smashing-plugin/smashing-plugin.php

    It seems to work just fine when I disable JavaScript in my browser though.

    Any idea how to solve this issue?

    Cheers,
    Max

    0
    • 24

      This looks like it might be a new development as of WordPress 4.2 – they updated the plugins page to allow you to update without leaving the page. I will look into it and post an update here and on the Git repo. Thanks for flagging and your compliments!

      0
    • 25

      Hey there, I resolved the issue. It turns out that the new update JS relies on the slug not having special characters. I added an update to the post to reflect this as well as updating the Git repo for the demo code.

      0

↑ Back to top