Deploy a Hugo Website with GIT
27 May 2019
Ettore Dreucci
4 minute read

[recipe, sysadmin]: How to deploy a hugo-powered static website to a webserver with GIT

This website is powered by Hugo, an open-source static website generator written in Go. I can write pages and posts in Markdown and then generate some static .html pages to upload to a webserver.

For this project, as I usually do, I push every change to a self-hosted GIT server with Gogs (another open-source, golang written GIT server), so why not use webhooks to automate the deployment?

Gogs, as GitHub, GitLab and other well-known services provides webhooks to notify other services of changes in a repository: they are basically HTTP callbacks triggered by some user-defined events such as a push or a pull request. Each time you, for instance, push to a repo, Gogs will send a HTTP POST payload to the webhook’s configured URL: the payload will contain the relevant event information.

To protect from unauthorized requests you could tell Gogs to send, in the header of the POST request, a X-Gogs-Signature that contains the HMAC hex digest of the payload generated using the sha256 hash function and a secret as the HMAC key.

My event-chain is the following: I push the new content to the repo, Gogs trigger a webook that send a HTTP POST payload to a .php page in the webserver. The php-page pulls the repo to the server, generates the static website with hugo and copies the newly generated public directory to the document root of the webserver.

So, let’s see the practical part:

  1. Add in the Gogs repository configuration an ssh Deploy Key, to pull the repo in the webserver. The deploy key will have read-only access. You can generate the ssh key with ssh-keygen but keep it password-less to make the pull completely automated.

  2. Clone the repo in the document root with

    git clone user@git.server:repo /var/www/
    
  3. Insert in your .htaccess file the secret like that:

    SetEnv GOGS_DEPLOY_SECRET y0uRs3cRetC0d3
    
  4. Create a deploy.php page that will pull the repo, generate the website and deploy it when called. You can use the following script:

    <?php
    /**
    * Automated deploy from Gogs
    *
    * Template from ServerPilot (https://serverpilot.io/community/articles/how-to-automatically-deploy-a-git-repo-from-bitbucket.html)
    * Hash validation from liogate (https://github.com/gogs/gogs/issues/4233#issue-211797295)
    */
      
    // Variables
    $secret = getenv('GOGS_DEPLOY_SECRET');
    $repo_dir = '/path/to/repo/';
    $web_root_dir = '/var/www/';
    $rendered_dir = '/public';
    $hugo_path = '/usr/local/bin/hugo';
      
    // Validate hook secret
    if ($secret !== NULL) {
    // Get signature
    $gogs_signature = $_SERVER['HTTP_X_GOGS_SIGNATURE'];
      
    // Make sure signature is provided
    if (!isset($gogs_signature)) {
        file_put_contents('deploy.log', date('m/d/Y h:i:s a') . ' Error: HTTP header "X-Gogs-Signature" is missing.' . "\n", FILE_APPEND);
        die('HTTP header "X-Gogs-Signature" is missing.');
    } elseif (!extension_loaded('hash')) {
        file_put_contents('deploy.log', date('m/d/Y h:i:s a') . ' Error: Missing "hash" extension to check the secret code validity.' . "\n", FILE_APPEND);
        die('Missing "hash" extension to check the secret code validity.');
    }
      
    // Get payload
    $payload = file_get_contents('php://input');
      
    // Calculate hash based on payload and the secret
    $payload_hash = hash_hmac('sha256', $payload, $secret, false);
      
    // Check if hashes are equivalent
    if (!hash_equals($gogs_signature, $payload_hash)) {
        // Kill the script or do something else here.
        file_put_contents('deploy.log', date('m/d/Y h:i:s a') . ' Error: Bad Secret' . "\n", FILE_APPEND);
        die('Bad secret');
    }
      
    };
      
    // Parse data from Gogs hook payload
    $data = json_decode($_POST['payload']);
      
    $commit_message;
    if (empty($data->commits)){
    // When merging and pushing to Gogs, the commits array will be empty.
    // In this case there is no way to know what branch was pushed to, so we will do an update.
    $commit_message .= 'true';
    } else {
    foreach ($data->commits as $commit) {
        $commit_message .= $commit->message;
    }
    }
      
    if (!empty($commit_message)) {
    // Do a git pull, run Hugo, and copy files to public directory
    exec('cd ' . $repo_dir . ' && git pull');
    exec('cd ' . $repo_dir . ' && ' . $hugo_path);
    exec('cd ' . $repo_dir . ' && cp -r ' . $repo_dir . $rendered_dir . '/. ' . $web_root_dir);
      
    // Log the deployment
    file_put_contents('deploy.log', date('m/d/Y h:i:s a') . " Deployed branch: " .  $branch . " Commit: " . $commit_message . "\n", FILE_APPEND);
    }
    
  5. In the Gogs repository webpage, activate a webhook, specifying the deploy.php URL, the content type (application/json) and a secret. Then select the event(s) that will trigger the webhook.

  6. You’re done! Write a new post, push it and see the magic ;)