Advertisement
  1. Code
  2. WordPress
  3. Plugin Development

Distributing Your Plugins in GitHub with Automatic Updates

Scroll to top

WordPress has made it convenient for plugins to be deployed to the public. Just put your plugin in the WordPress plugin repository and it can be easily found by people from inside their dashboard.

Probably one of the best features of the repository is that it makes it easy to update plugins from inside the dashboard. Update notifications are displayed inside the dashboard, and performing an update is as easy as clicking the Update Now button.

But how about if you wanted to host the plugin on your own? The convenient update feature won't be available if you host your plugin outside the WordPress plugin repo. Nor would your users be notified of an available update.

This article will teach you that with a little creative coding, you can host your own WordPress plugins in GitHub while still retaining the automatic update feature.

Why Host Your Plugin in GitHub?

There may be a number of reasons why you would want to host your plugin outside the WordPress plugin repository. One of the main restrictions the repository gives is that you are required to license your plugin as GPLv2. I won't dwell on the details of licensing, but in a nutshell it means that anyone can share your work. So if you want to sell your plugin, then hosting it in a private GitHub repository is now an option you can consider.

However, because of the way WordPress was built, hosting your plugin in GitHub would disable automatic updates for our plugin. To understand why this is the case, we have to understand how WordPress handles plugin updates.

You can learn more about WordPress plugin repository by reading their Detailed Plugin Guidelines.

How WordPress Updates Plugins

First, let's investigate  how WordPress performs plugin updates in order to get a better understanding of why self-hosted plugins can't have automatic updates

WordPress periodically checks the WordPress repository for updates your installed plugins. It receives a bunch of information on each plugin such as its latest version and the plugin's package URL. This is what we usually see in the plugin admin page when we are notified of an update:

Plugin Admin Update NowPlugin Admin Update NowPlugin Admin Update Now

When we click the View version x.x details link, WordPress performs another lookup in the plugin repo. This time it gets more detailed information about the plugin like it's description, changelog, what WordPress version its tested in, and plenty more. These information are shown to us in a lightbox:

Plugin LightboxPlugin LightboxPlugin Lightbox

Lastly, when the Update Now link is clicked, the updated plugin package is downloaded and installed.

So why doesn't automatic updates work for self-hosted plugins? It's because WordPress tries to find it in the WordPress plugin repository and can't find it there!

The Game Plan

Our plan is to enable automatic updates with our GitHub-hosted plugin.

Here's a list of what we need to do to make it work:

  • We need a way to deploy plugin updates in GitHub.
  • We should show plugin version update notifications,
  • It would be nice to display plugin details when the View version x.x details link is clicked.
  • We also want to successfully install the plugin update when the update now link is clicked,

How to Execute the Plan

We will be using some WordPress filters in order for us to implement our game plan. These are:

  • pre_set_site_transient_update_plugins. This filter is called when WordPress tries to check for plugin updates.
  • plugins_api. This one is used when WordPress shows the update details of the plugin.
  • upgrader_post_install. Lastly, this is called after a plugin is successfully installed.

We are going to hook into these filters, then push our data into the results to make WordPress think that our plugin is in the WordPress plugin repository. The data we will put in will come from our GitHub repo, and should mimic the data given out by the plugin repository.

Setting Up the GitHub Project

Before we go ahead with coding, let's first talk about GitHub and how we'll use it to give out the data that we need to feed WordPress.

You will need either a private or a public GitHub repository. Your repo should contain all of your plugin files, and not a compressed copy of your plugin.

We will be using a cool feature of GitHub called Releases.

GitHub ReleasesGitHub ReleasesGitHub Releases

The good thing about releases is that it gets the current codebase in the repository and creates a downloadable zip file for each specific release. We can tell WordPress to download this zip file when updating our plugin.

Another good thing about Releases is that we can put in our plugin update details in the release notes. We can then parse this and display it in the lightbox WordPress shows for plugin update details. We can go further and even allow GitHub markdown for our changelog.

GitHub ReleasesGitHub ReleasesGitHub Releases

When it's time to deploy an update to our plugin, follow the formatting in the image above when you are creating a new release:

  • Tag Name: Plugin version (just the number)
  • Release notes: The description of the update
You can read more about GitHub Releases in their article Release Your Software

Creating our Updater Class

Now it's time to code our plugin!

First, we create the starting point for our class:

1
class BFIGitHubPluginUpdater {
2
3
    private $slug; // plugin slug

4
    private $pluginData; // plugin data

5
    private $username; // GitHub username

6
    private $repo; // GitHub repo name

7
    private $pluginFile; // __FILE__ of our plugin

8
    private $githubAPIResult; // holds data from GitHub

9
    private $accessToken; // GitHub private repo token

10
11
    function __construct( $pluginFile, $gitHubUsername, $gitHubProjectName, $accessToken = '' ) {
12
        add_filter( "pre_set_site_transient_update_plugins", array( $this, "setTransitent" ) );
13
        add_filter( "plugins_api", array( $this, "setPluginInfo" ), 10, 3 );
14
        add_filter( "upgrader_post_install", array( $this, "postInstall" ), 10, 3 );
15
16
        $this->pluginFile = $pluginFile;
17
        $this->username = $gitHubUsername;
18
        $this->repo = $gitHubProjectName;
19
        $this->accessToken = $accessToken;
20
    }
21
22
	// Get information regarding our plugin from WordPress

23
    private function initPluginData() {
24
        // code here

25
    }
26
27
	// Get information regarding our plugin from GitHub

28
    private function getRepoReleaseInfo() {
29
        // code here

30
    }
31
32
	// Push in plugin version information to get the update notification

33
    public function setTransitent( $transient ) {
34
        // code here

35
        return $transient;
36
    }
37
38
	// Push in plugin version information to display in the details lightbox

39
    public function setPluginInfo( $false, $action, $response ) {
40
        // code ehre

41
        return $response;
42
    }
43
44
	// Perform additional actions to successfully install our plugin

45
    public function postInstall( $true, $hook_extra, $result ) {
46
        // code here

47
        return $result;
48
    }
49
}

This is the class structure we're going to use. We just mainly defined all the functions we're going to use and created our filter hooks. This class does nothing as of the moment, except to assign values to class properties.

The Constructor Arguments

For our class to execute, we will need a few arguments:

  • $pluginFile: We will be calling this class from our main plugin script, this should have the value __FILE__. We will be getting details about our plugin from this later on.
  • $gitHubUsername: Your GitHub username
  • $gitHubProjectName: Your GitHub repository name
  • $accessToken: An access token that will allow us to view the details of a private GitHub repo. If your project is hosted in a public GitHub repo, just leave this blank.

Now let's fill up the functions of our class with some code.

You can learn more about creating access tokens from GitHub help. You can also read up on how the GitHub API works.

The initPluginData Function

This is the simplest function in our class. We will be needing our plugin's slug and other information throughout the rest of the script, so we're putting the necessary calls in a function for convenience.

1
$this->slug = plugin_basename( $this->pluginFile );
2
$this->pluginData = get_plugin_data( $this->pluginFile );

The getRepoReleaseInfo Function

This function is all about communicating with GitHub to get our release information. We will use the GitHub API to get details regarding our latest release. Then store everything that we get in our githubAPIResult property for future processing.

The pre_set_site_transient_update_plugins filter is called twice by WordPress, once when it checks for plugin updates, then another after it gets results. Since we will be using this function in that filter, we would be querying the GitHub API twice. We just need to get information from GitHub once:

1
// Only do this once

2
if ( ! empty( $this->githubAPIResult ) ) {
3
    return;
4
}

Next, we will be using the GitHub API to get information about our releases:

1
// Query the GitHub API

2
$url = "https://api.github.com/repos/{$this->username}/{$this->repo}/releases";
3
4
// We need the access token for private repos

5
if ( ! empty( $this->accessToken ) ) {
6
    $url = add_query_arg( array( "access_token" => $this->accessToken ), $url );
7
}
8
9
// Get the results

10
$this->githubAPIResult = wp_remote_retrieve_body( wp_remote_get( $url ) );
11
if ( ! empty( $this->githubAPIResult ) ) {
12
    $this->githubAPIResult = @json_decode( $this->githubAPIResult );
13
}

Lastly, we'll only keep the data for latest release of the plugin:

1
// Use only the latest release

2
if ( is_array( $this->githubAPIResult ) ) {
3
    $this->githubAPIResult = $this->githubAPIResult[0];
4
}

Now we can get our plugin data from GitHub. We will be parsing this data in the succeeding functions.

The setTransitent Function

This function is called when WordPress checks for plugin updates. Our job here is to use our GitHub release data to provide information for our plugin update.

The first thing we do is to check whether WordPress has already checked for plugin updates before. If it has, then we don't have to run the rest of the function again.

1
// If we have checked the plugin data before, don't re-check

2
if ( empty( $transient->checked ) ) {
3
    return $transient;
4
}

Next, we will get the plugin information that we are going to use:

1
// Get plugin & GitHub release information

2
$this->initPluginData();
3
$this->getRepoReleaseInfo();

After calling these two functions, we can check our local plugin's version from the version (the tag name) found in GitHub. We can use PHP's convenient version_compare function to compare the two values:

1
// Check the versions if we need to do an update

2
$doUpdate = version_compare( $this->githubAPIResult->tag_name, $transient->checked[$this->slug] );

Lastly, there is a plugin update available, we need to prompt the admin to display an update notification. We do this by filling up the $transient variable with our updated plugin information.

1
// Update the transient to include our updated plugin data

2
if ( $doUpdate == 1 ) {
3
    $package = $this->githubAPIResult->zipball_url;
4
5
    // Include the access token for private GitHub repos

6
    if ( !empty( $this->accessToken ) ) {
7
        $package = add_query_arg( array( "access_token" => $this->accessToken ), $package );
8
    }
9
10
    $obj = new stdClass();
11
    $obj->slug = $this->slug;
12
    $obj->new_version = $this->githubAPIResult->tag_name;
13
    $obj->url = $this->pluginData["PluginURI"];
14
    $obj->package = $package;
15
    $transient->response[$this->slug] = $obj;
16
}
17
18
return $transient;

After this function processes our GitHub information, our admin would be able to display notifications in the plugin admin page:

Plugin Update NotificationPlugin Update NotificationPlugin Update Notification

The setPluginInfo Function

This function's purpose is to gather details regarding the updated plugin from the release notes. All these information will be displayed inside a lightbox when the View version x.x details link is clicked.

First, let's get our plugin information:

1
// Get plugin & GitHub release information

2
$this->initPluginData();
3
$this->getRepoReleaseInfo();

Next we check whether or not it is out time to display anything. We can check if we are trying to load information for our current plugin by checking the slug:

1
// If nothing is found, do nothing

2
if ( empty( $response->slug ) || $response->slug != $this->slug ) {
3
    return false;
4
}

To display our plugin details, we need to add our plugin information manually to the $response variable, normally this variable would be populated with the results from the WordPress plugin repository:

1
// Add our plugin information

2
$response->last_updated = $this->githubAPIResult->published_at;
3
$response->slug = $this->slug;
4
$response->plugin_name  = $this->pluginData["Name"];
5
$response->version = $this->githubAPIResult->tag_name;
6
$response->author = $this->pluginData["AuthorName"];
7
$response->homepage = $this->pluginData["PluginURI"];
8
9
// This is our release download zip file

10
$downloadLink = $this->githubAPIResult->zipball_url;
11
12
// Include the access token for private GitHub repos

13
if ( !empty( $this->accessToken ) ) {
14
    $downloadLink = add_query_arg(
15
        array( "access_token" => $this->accessToken ),
16
        $downloadLink
17
    );
18
}
19
$response->download_link = $downloadLink;

So far we've added our plugin details, but we haven't yet parsed our release notes from our GitHub release. Let's check what we have in our release notes:

GitHub ReleasesGitHub ReleasesGitHub Releases

In the release notes, we have specified three details regarding our release: our changelog, followed by the minimum required WordPress version, then the latest WordPress version the plugin was tested in. We're going to parse this text and extract these values.

Since we're hosting our plugin in GitHub, it would be nice if we can have the ability to use GitHub markdown in our release notes. I'm going to use a PHP class called ParseDown to convert the markdown text to HTML:

1
// We're going to parse the GitHub markdown release notes, include the parser

2
require_once( plugin_dir_path( __FILE__ ) . "Parsedown.php" );

We're also going to create tabs in the lightbox to make it conform with how WordPress repository hosted plugins display their information. One will be for the plugin description and the other will be for our changelog:

1
// Create tabs in the lightbox

2
$response->sections = array(
3
    'description' => $this->pluginData["Description"],
4
    'changelog' => class_exists( "Parsedown" )
5
        ? Parsedown::instance()->parse( $this->githubAPIResult->body )
6
        : $this->githubAPIResult->body
7
);

Finally we're going to extract the values for the requires and tested:

1
// Gets the required version of WP if available

2
$matches = null;
3
preg_match( "/requires:\s([\d\.]+)/i", $this->githubAPIResult->body, $matches );
4
if ( ! empty( $matches ) ) {
5
    if ( is_array( $matches ) ) {
6
        if ( count( $matches ) > 1 ) {
7
            $response->requires = $matches[1];
8
        }
9
    }
10
}
11
12
// Gets the tested version of WP if available

13
$matches = null;
14
preg_match( "/tested:\s([\d\.]+)/i", $this->githubAPIResult->body, $matches );
15
if ( ! empty( $matches ) ) {
16
    if ( is_array( $matches ) ) {
17
        if ( count( $matches ) > 1 ) {
18
            $response->tested = $matches[1];
19
        }
20
    }
21
}
22
23
return $response;

The postInstall Function

This last function we'll deal with performing additional processes that we need to fully install our plugin after it is downloaded.

When creating a release for our GitHub repo, it automatically creates a zip file for that specific release. The filename for the zip file is generated by GitHub with the format reponame-tagname.zip. This also contains a directory where our plugin files are located. Similarly, the directory name for this also follows format reponame-tagname.

Normally, when WordPress downloads and unzips a plugin archive, the plugin directory name doesn't change. If you're plugin's directory is my-awesome-plugin, after deleting the old plugin files and unzipping the updated one, you're directory would still be named my-awesome-plugin. But since GitHub changes our plugin's directory name every time we deploy a release, WordPress won't be able to find our plugin. It would still be able to install it, but it won't be able to re-activate it. We can fix this issue by renaming the new directory to match the old one.

First thing's first:

1
// Get plugin information

2
$this->initPluginData();

Next we have to check whether our plugin is currently activated, so we can re-activate it afterwards:

1
// Remember if our plugin was previously activated

2
$wasActivated = is_plugin_active( $this->slug );

Now we rename our updated plugin directory to match the old one. We're using the function move here, but since we are specifying the same directory, it would be just like renaming it:

1
// Since we are hosted in GitHub, our plugin folder would have a dirname of

2
// reponame-tagname change it to our original one:

3
global $wp_filesystem;
4
$pluginFolder = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . dirname( $this->slug );
5
$wp_filesystem->move( $result['destination'], $pluginFolder );
6
$result['destination'] = $pluginFolder;

The last step would be to re-activate the plugin:

1
// Re-activate plugin if needed

2
if ( $wasActivated ) {
3
    $activate = activate_plugin( $this->slug );
4
}
5
6
return $result;

Calling Our GitHub Updater Class

Now that the class is finished, all that's left to do is to call it in our main plugin file:

1
require_once( 'BFIGitHubPluginUploader.php' );
2
if ( is_admin() ) {
3
    new BFIGitHubPluginUpdater( __FILE__, 'myGitHubUsername', "Repo-Name" );
4
}

Trying It Out

That's it! Just include this class and call it in your plugin, and it should start checking for updates automatically.

To test if it works, create a new release for your GitHub repo and follow the guidelines stated earlier:

GitHub ReleasesGitHub ReleasesGitHub Releases

Once you have created your release, you can force WordPress to check for updates by clicking the refresh button found in the admin bar.

Conclusion

I hope you learned a thing or two about how WordPress works and how the whole process of updating plugins is performed. You can download the complete script from the download links at the top of this article.

I hope you enjoyed this article. I highly appreciate any feedback, comments and suggestions. Share your thoughts below!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.