Pull Request Preview

Use Case

Providing a preview of a pull request to share with team members allows them to review changes and give feedback without disrupting their local working environment. In this guide, we’ll build a tool using Stackery that can transform a pull request from GitHub into a previewable UI.

How it Works

The stack in this guide uses four node types: Rest Api, Function, Object Store, and CDN. When a GitHub PR triggers an action: “opened”, “reopened”, “edited”, or “synchronize”, the webhook attached to the PR repository will trigger a chain of events, resulting in a previewable remote build. The hook will send a request containing the PR body to the Rest Api, which will trigger the Function runBuild to clone and compile the source code, then upload the files to an Object Store. Lastly, the CDN is added to provide secure access to the build.

For additional context, have a look at our previous blog post, outlining the initial approach and goals of the tool.


How To

Stack Setup

  1. Create a new stack in your Stackery dashboard
  2. Select a Git provider
    1. The example code is stored in GitHub but either provider will work
    2. Name the stack “pr-preview” (or any name of your choosing)
  3. Delete all the nodes in the new canvas by multi-selecting them (cmd+click) and pressing the Delete (Mac) / Backspace (Windows) key on your keyboard.
  4. Add a Rest Api node
    1. Set name to Endpoint (node names are your choice but we’ll explicitly assign them to eliminate guess work)
    2. Set endpoint to /pr-build
  5. Add a Function node
    1. Set name to runBuild (name defines the Function directory in the code repository)
    2. Set timeout to 300
    3. Drag a wire from the Rest API node (above) to this node
  6. Add an Object Store node
    1. Set name to Build
    2. Set public permissions to read
    3. Drag a wire from the Function node (above) to this node
  7. Add a CDN node (an Object Store may be enough if the finished PR build doesn’t require authentication)
    1. Set name to Content
    2. Drag a wire from this node to the Object Store node (above)
  8. Deploy the stack
    We will use values from the deployed nodes (from the Deployment tab) to assign build variables in our scripts
    1. Click Commit to Git
    2. Click Prepare Deployment
    3. Click Deploy (when prepare is finished)
  9. Clone the stack locally with Git (from GitHub or AWS, depending on your choice above)

Your stack should resemble this:

Stack layout

Webhook Setup:

  1. In GitHub, create or navigate to the repository that will contain the pull requests
  2. Select the Settings tab
  3. Select Webhooks from the left nav
  4. Click Add webhook
  5. Set Payload Url using the Rest Api node’s domain and endpoint
    1. Open the node deployment properties panel in Stackery
      From the dashboard, click into the Deployments tab, then click the Rest Api node of the current deployment
    2. Copy the DNS Name and Endpoint values
    3. Paste the copied values into the Payload Url field in GitHub
  6. Click Let me select individual events
  7. Click Pull request
  8. Click Add webhook

This hook should now be present (with the Rest Api endpoint) in the webhooks list.

Dependency Setup:

In order to run Git commands, manipulate the Lambda file system, and glob build files for upload, the function requires two dependencies in addition to what comes packaged with Node.js (the default Function runtime). Let’s clone the stack so we can work on it locally, then install the dependencies:

  1. Clone the stack locally (git clone <stack url>)
  2. cd to the runBuild directory
  3. Copy the /bin directory from the example repo to provide Git as a Lambda executable
    Stackery packages /bin contents and makes them available in the PATH environment variable for easy executable support
  4. Install (or copy) package dependencies
    or copy package.json from the example repo and skip the next 2 steps
    1. npm install fs-extra
    2. npm install glob

runBuild Handler Function:

The stack will invoke this function each time the endpoint defined in the Rest Api node is hit with a POST request from the GitHub webhook. The contents of that request will provide everything we need to assemble the build.

Let’s add some code so the function can clone the PR, run npm commands, then output a build (using Stackery.output) to a Stackery Object Store:

  1. Open the Function node and paste the following code into the function body
    or copy the source code from the example repo
  2. Update the string values (containing “MODIFY: “ to match your settings
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
const child_process = require('child_process');
const path = require('path');
const fs = require('fs-extra');
const glob = require('glob');

function spawnPromise(command, args, options) {
  args = args || [];
  options = options || {};

  if (!options.env) {
    options.env = {};

  Object.assign(options.env, process.env);

  return new Promise((resolve, reject) => {
    child_process.execFile(command, args, options, (err, stdout, stderr) => {
      if (err) {
        err.stdout = stdout;
        err.stderr = stderr;

      } else {
        resolve({ stdout: stdout, stderr: stderr });

module.exports = function(event, context, callback) {
  let body;

  try {
    body = JSON.parse(event.body.toString());
  } catch (err) {
    throw new Error(
      `Failed to parse message body:\n${event.body.toString()}`

  if (
    body.action !== 'opened' &&
    body.action !== 'edited' &&
    body.action !== 'reopened' &&
    body.action !== 'synchronize'
  ) {
    console.log(`PR was ${body.action} and will not trigger a new build`);
    return Promise.resolve();
  const token = process.env.GITHUB_ACCESS_TOKEN
  const prNumber = body.pull_request.number;
  const branch = body.pull_request.head.ref;
  const repo = body.pull_request.head.repo.full_name;
  const localRepoDir = '/tmp/repo';
  const cdnUrl = `https://<MODIFY: link to CDN node domain>/${prNumber}/index.html`;
  const postUrl = `https://api.github.com/repos/<MODIFY: repo owner>/<MODIFY: repo name>/issues/${prNumber}/comments`;


  return spawnPromise('./run.sh', [`https://${token}@github.com/${repo}.git`, branch, prNumber, localRepoDir])
    .then(() => {
      let files = glob.sync('**/*', {
        cwd: `${localRepoDir}/build`,
        nodir: true,
        dot: true

      const promises = files.map(file => {
        const patternHtml = /\.html$/i;
        const patternJs = /.*\.js$/;
        const patternCss = /.*\.css$/i;
        const patternSvg = /.*\.svg$/i;
        const metadata = {};

        if (patternHtml.test(file)) {
          metadata['Content-Type'] = 'text/html';
          metadata['Cache-Control'] = 'no-cache';
        } else if (patternCss.test(file)) {
          metadata['Content-Type'] = 'text/css';
        } else if (patternJs.test(file)) {
          metadata['Content-Type'] = 'application/javascript';
        } else if (patternSvg.test(file)) {
          metadata['Content-Type'] = 'image/svg+xml';

        let params = {
          Body: fs.readFileSync(`${localRepoDir}/build/${file}`),
          Key: `${prNumber}/${file}`,
          Bucket: ports[0][0].bucket
        return s3.putObject(params).promise();

      return Promise.all(promises);
    .then(message => {
      if (body.action === 'opened') {
        return spawnPromise('./update.sh', [token, cdnUrl, postUrl]);
    .then(() => {
      callback(null, {
        statusCode: 204,
        headers: {},
        body: JSON.stringify({}) 
    .catch(err => {
      console.log('error', err);

When finished, the Function node will send a request back to GitHub, adding a comment to the PR with a url to preview the build on the CDN.

Shell Scripts:

The runBuild function uses two scripts when invoked that perform the steps required to: clone the repo (from the PR webhook response), install dependendencies, compile a static build, and make a POST request back to GitHub with a link to the preview. It’s assumed that the PR repo will have npm commands install and build configured. Alter the run.sh script for your project’s needs.

  1. In the Function directory (/runBuild), create the following two scripts:
    or copy run.sh and update.sh from the example repo


set -x
set -e 


echo 'localRepoDir: ' $localRepoDir ', repo: ' $repo ', branch: ' $branch ', pr: ' $pr 

cd "$localRepoDir"

git init

git remote add origin $repo

git fetch origin $branch

git reset --hard FETCH_HEAD 

npm install --no-progress --loglevel=error --cache '/tmp/npm' --userconfig '/tmp/npmrc'

npm run build


set -x
set -e 


curl --user "stack-bot:$token" -H "Content-Type: application/json" --request POST --data '{"body": "Build preview: \n '"$cdnUrl"'"}' $postUrl


Now that all the pieces are in place, test it out by opening a pull request in the repository with the webhook defined. The Function deployment properties can be found by double-clicking the Function in the Stackery dashboard “Deployments” view. Click “Logs” under “Metrics & Logs” to trace the Function in CloudWatch (adding console.log in the runBuild Function will output to this view).

Try Stackery For Free

Gain control and visibility of your serverless operations from architecture design to application deployment and infrastructure monitoring.