Continuous delivery for your Ghost Theme


I've had so many drafts and blog post ideas about ghost. However, they never went through. Why? Most of it was related to me self hosting it and I was wondering if the real solution wouldn't be to just switch to Ghost Pro. Guess what ? We just did it!

End of story.

No I'm kidding. My last hosting on azure had one advantage, every time I was editing the theme it would be built, a docker image would be pushed to a repository and Azure would pick up on this and update the blog.

By switching to Ghost Pro I've lost that. I was back to uploading my theme manually through my browser

Automation

After a couple of manual updates I got beyond tired of doing it. The monkey in myself thought there must be something clever to do.

I already has a bunch of gulp tasks. I remembered that chrome was offering a convenient node library to control a headless Chrome: Puppeteer. I always wanted to play around with it. Here was my cue!

Puppeteer is a carefully designed automation library for Chrome headless. It's so good that I got the upload steps scripted in a matter of minutes:

var argv = require('minimist')(process.argv.slice(2));

gulp.task('publish', function (done) {
    (async () => {
        const browser = await puppeteer.launch();
        try {
            const page = await browser.newPage();
            await page.goto(argv.url + '/admin');
            await page.screenshot({ path: '.debug/login.navigated.png' });

            await page.waitFor('input[name=identification]')
            await page.screenshot({ path: '.debug/login.loaded.png' });

            await page.type('input[name=identification]', argv.login);
            await page.type('input[name=password]', argv.password);
            await page.screenshot({ path: '.debug/login.filled.png' });
            await page.click('button.login');
            await page.waitForNavigation({ waitUntil: 'networkidle0' });
            await page.screenshot({ path: '.debug/login.done.png' });

            await page.goto(page.url() + 'settings/design', { waitUntil: 'networkidle0' });
            await page.screenshot({ path: '.debug/theme.page.png' });

            await page.waitFor('a.gh-themes-uploadbtn')
            await page.click('a.gh-themes-uploadbtn');
            await page.screenshot({ path: '.debug/theme.upload.png' });
            var themeUpload = await page.$('input[type=file]');
            await themeUpload.uploadFile('dist/gl-casper.zip');
            await page.screenshot({ path: '.debug/theme.file.png' });

            await page.waitFor('button.gh-btn-red');
            await page.click('button.gh-btn-red');
            await page.screenshot({ path: '.debug/theme.uploading.png' });

            await page.waitFor("//h1[contains(.,'Upload successful!')]", {})
            await page.screenshot({ path: '.debug/complete.png' });

            done();
        }
        catch (error) {
            console.log(error);
            done(error);
        }
        finally {
            await browser.close();
        }
    })();
});

As you can see, it navigates to the backoffice, logs in, navigate to the design section, uploads the file and waits for the success message. It even takes screenshot to help debugging any issue.

Job's done!

More automation

Well that was pretty good but I still had to invoke it, type my password etc... Enough! A clever monkey is a lazy monkey!

Puppets are cute, but they would be much cooler on a big fucking rocket, wouldn't they?

But what do I mean by rocket? Well that's dead simple I want to be able to easily integrate this into a Visual Studio Team Services Release pipeline. So I wrote an extension to bring that straight into your account. Wait you are using Github? Well, vsts work perfectly with github, how do you think we build our Github project?

Ghost theme uploader

The build pipeline

Ok, let's back to getting shit done! If you don't have a VSTS account go create one, it's free! If you don't have a project yet, create one!

Note that this part is based on the tooling provided in the builtin casper theme. If you created you own tooling you may need to adjust a few steps.

Now just head to the Build section. and hit the New definition button

insert a screenshot of the new UI

First you will be invited to configure the source repository. Select yours, if it is on github or even bitbucket, no problem you can do it. Then select empty build.

Now hit the plus button on Phase 1 and search for Yarn.

Then, add on Yarn Tool Installer and also two Yarn tasks.

Finally search for Publish Artifact and add one. You should have something close to that:

Allright, now we have our blocks we need to configure them. Let's start with the installer. The defaults should be fine.

So let's move on to the next step. Oh once again the default are perfect (yes yarn is handy). Next !

Now we need to package our them, this is done by invoking the zip script. just add zip to the arguments.

The last thing to do is to publish our package as an artifact. An artifact is just a bunch of files attached to a build.

Now we just have to turn on Continuous integration and our theme we will always have our latest package built ready to be used.

Just head to the Triggers tab and tick the Continuous integration box! You can even tweak the branches.

Now just Save and Queue. And it will start a build.

Release pipeline

OK, but now we have a zip, we want to push it to our blog. To achieve that we will use release management.

Credits
Rockets by SpaceX on Unsplash
Puppets by Sagar Dani on Unsplash
Rocket (again) by SpaceX on Unsplash