Source Operations Sorcery: putting the flavor back into vanilla WordPress

Chad Carlson
Chad Carlson
Manager, Developer Relations
04 Mar 2021

Last year, we released a new feature called Source Operations—an addition to your .platform.app.yaml configuration that allows you to specify commands that can commit changes to your project’s repository when called. For example, Source Operations can be used to update your dependencies, even hooking a composer update to a cron job so that dependency updates occur automatically on a dedicated environment.

It got us thinking: What other magic could our users do with Source Operations? So we conjured this series to answer that question. The first rabbit we’re pulling out of the Source Operations hat: performing automatic updates to WordPress when it isn’t managed by Composer.

Vanilla waiver

We refer to this case as vanilla WordPress; it’s set apart by its points of incompatibility from the way we normally do things at Platform.sh. WordPress developers are used to being able to SFTP into a server running WordPress to make edits or use the CLI to get updates. But WordPress’s auto_update_core function just really doesn’t work with the read-only filesystems you get on our app containers.

For that reason, we disable WP_AUTO_UPDATE_CORE on all of our WordPress templates and recommend developers use Composer. If you must go the vanilla route, you’re restricted to “update locally, commit, and push.”

But with Source Operations, it’s possible to get access to a writable file system, checked out to the current branch, and make these kinds of changes. Should you do this? Well, there are plenty of caveats and edge cases we’ve yet to discover. Perhaps it’s best to think of this as a “look what Source Operations can do” type of article, after which we can figure out together what best practices should be for these operations, as well as their limitations.

Update Wordpress

Setting up

To start off with, we deploy the WordPress “Vanilla” template. If you’re trying to migrate your own WordPress site that isn’t managed with Composer, this template and its complement guide are going to be the best resources for doing so. The template itself provides database credentials automatically, so when it has fully deployed we can go through the WordPress installer to create an admin account and login. Then with the project ID, clone the repo locally:

$ platform get <PROJECT_ID>

Once you’ve logged into the dashboard, there will be the “Updates” section in the sidebar. When an update for a plugin, a theme, or WordPress core is available, you’ll see a counter notification on this tab. Normally it’s at this point that we’d advise you to pull, update, commit, and push to a new environment to test those updates.

But let’s try it with Source Operations instead this time.

CLI and authentication

Performing the update is going to require the use of the Platform.sh CLI within the environment and, therefore, an API token. Obtain a token from your “Accounts” page of the management console, and then (with the CLI installed locally) create a project-level environment variable with that token:

$ platform variable:create -l project --prefix env: --name PLATFORMSH_CLI_TOKEN --value "API_TOKEN" --json N --sensitive y --visible-build y --visible-runtime y

We’re still going to add some restrictions that keep the operation from running on Master in the next section, but setting the variable project-wide allows us to make it visible at build time, which is the level we’ll need for it to be visible during a Source Operation. It’s at this point that we have our first big caveat: How safe is it to include an API token in your project like this?

Well, if it’s just you working on the project, then there really isn’t a problem here. But in practice this rarely happens, and your personal API token—although not visible through the management console—will be visible to anyone with SSH access to any of the environments on the project. Someone could use that token, if belonging to the project owner, and delete the site if they were so motivated. Which is not great.

We generally recommend using API tokens that belong to machine users, a Platform.sh account given restricted permissions to the project and whose only purpose is to run automation tasks like this.

After you’ve added the token as an environment variable, you can add the Platform.sh CLI as a build dependency to your .platform.app.yaml file:

dependencies:
   php:
       wp-cli/wp-cli: "^2.2.0"
       wp-cli/wp-cli-bundle: "^2.4"
       psy/psysh: "^0.10.4"
       platformsh/cli: "^3.65.0"

In other tutorials we’ve instructed you to download the CLI in your build hooks, but it turns out that won’t really work as expected here. Source Operations take place on file systems at a state just before the build hook runs. So build dependencies are available, but if the CLI was installed later, it wouldn’t be. If you’d rather not include the CLI as a build dependency in this way, you can still install it during your build hooks. But you’ll also need to run the same installation command within the Source Operation definition as well before running any platform commands.

The update operation

In your .platform.app.yaml file, add the following block:

source:
   operations:
       update-wordpress:
           command: |
               set -e

               # Open a tunnel to the current environment, which allows us to get database credentials.
               ENVIRONMENT=$(git rev-parse --abbrev-ref HEAD)
               platform tunnel:open -p $PLATFORM_PROJECT -e $ENVIRONMENT -y

               # Export the relationships object and then our database credentials to the environment.
               export PLATFORM_RELATIONSHIPS="$(platform tunnel:info --encode)"
               export DB_NAME=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].path")
               export DB_HOST=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].host")
               export DB_PORT=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].port")
               export DB_USER=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].username")
               export DB_PASSWORD=$(echo $PLATFORM_RELATIONSHIPS | base64 --decode | jq -r ".database[0].password")
               export DB_HOST=$DB_HOST:$DB_PORT

               # Update WordPress with the WP CLI.
               wp core --path=$PLATFORM_SOURCE_DIR/wordpress update
               wp plugin --path=$PLATFORM_SOURCE_DIR/wordpress update-all
               wp theme update --path=$PLATFORM_SOURCE_DIR/wordpress --all

               # Stage changes, committing only when updates are available.
               git add .
               STAGED_UPDATES=$(git diff --cached)
               if [ ${#STAGED_UPDATES} -gt 0 ]; then
                   git commit -m "Gloriously autoupdated Wordpress."
               else
                   echo "No WordPress updates found."
               fi
              
               # Close the connection.
               platform tunnel:close -y

So we’ve got an interesting block of YAML here—let’s pick it apart. When the operation is run, we’re actually going to use the CLI to open a tunnel to the environment and use that tunnel to mimic the relationships array locally, same as we would in a tethered local development scenario. Having our database credentials available allows us to then run our update commands with the WordPress CLI.

Once we’ve done that, we run the update. Then we’ve got a little catch block here that checks if file changes occurred on the repository after the update. This is here because we’ve noticed that without it running, an update command that results in no available updates (nothing to commit, working tree clean should be great news) will sometimes cause the operation to hang there when we try to commit. This block checks if there’s anything staged for commit and just exits when everything is up-to-date. Then we close our tunnel to clean up after ourselves.

After we commit and push the operation to the project, let’s create a new branch where we can dedicate to testing updates.

$ platform environment:branch update-wordpress

Once that’s successfully deployed, we can at any time run the operation on that environment:

$ platform source-operation:run update-wordpress

You’ll see that the operation will run and, if updates are available on any plugins, themes, or WordPress core, commit them to the environment, ready for testing.

Automatic updates

It’s great that we’ve created a new endpoint for the project to look for and to apply updates to our vanilla WordPress site. But I’m not going to remember to do this regularly, and since I expect you and I aren’t made of different stuff, let’s automate this.

Add the following to .platform.app.yaml:

crons:
   auto-update-wordpress:
       spec: '0 1 * * *'
       cmd: |
           if [ "$PLATFORM_BRANCH" = update-wordpress ]; then
               platform environment:sync code data --no-wait --yes
               platform source-operation:run update-wordpress --no-wait --yes
           fi

Now we have a cron job that runs everyday at 1am, syncs code and data from our production site, and then runs our update operation. We’ve even included some branch restrictions so all of our updates happen in the same place—only on our dedicated updates environment.

Now that we’ve shown you the secrets to this Source Operations trick, we hope you’ll give it a try and then report back to us whether it was a showstopper or just a puff of smoke. In our next Source Operations Sorcery article, we’ll pull back the curtains on how to use Source Operations to expand on the decoupled CMS pattern, updating a fleet of Gatsby presentation apps from a single Drupal content store.