bapistrano

Deploy JS apps to S3 multiverse.

Usage no npm install needed!

<script type="module">
  import bapistrano from 'https://cdn.skypack.dev/bapistrano';
</script>

README

bapistrano

An opinionated way of deploying JS apps to s3.

Bap is designed to be easily hooked up with a Continuous Deployment system that would keep all branches always built and uploaded to s3.

Installation

$ npm install --save-dev bapistrano

Usage

Typical usage is

$ bap release

This will build, upload and release your project. However, you can get more control by combining the following 2 commands

$ bap upload
$ bap release

For release branches - upload command only uploads the code without updating the current pointer. For feature branches - upload command uploads the code and updates the current pointer. The reasoning here is that in your CI configuration, you only need to specify bap upload to get continuous releases of all your branches, with the exception that the release branches only get uploaded, but not released. This way, if you then execute bap release at a later time (to a chat bot or in your terminal) - the release will be instant, since bap upload has already been executed in the background by CI.

All Commands

Build and upload the current branch. Uploading doesn't affect the current pointer.

$ bap upload
$ bap upload --as-branch master

Update the current pointer of current branch to the latest commit of current branch.

$ bap release
$ bap release --as-branch master

Rollback the current release of current branch to the previous release.

$ bap rollback
$ bap rollback --forward
$ bap rollback --as-branch master

List all deployments.

$ bap list
$ bap list --limit 100
$ bap list master

Config

Your aws credentials will be read from ~/.aws or environment variables (this is using s3fs under the hood which is using aws-sdk).

You can specify bapistrano configuration in package.json:

{
  "bap": {
    "bucket": "ui",
    "region": "eu-west-1",       # default is us-west-1
    "distDir": "build",          # default is `.`
    "uploadTo": "ui/app",        # default is package.json#name
    "build": false,              # default is "npm run build"
    "clean": false,              # default is "npm run clean"
    "notify": 'make slack',      # default is false
    "keepReleases": 5,           # default is -1, which keeps all releases
    "keepUploads": 5,            # default is 5
    "private": ["*.js.map"],     # default is [], which means make all files public
    "releaseBranches": [         # default is ["master"]
      "master",
      "next"
    ]
  }
}

What does this do exactly?

Bap considers there to be two types of branches. Release branches and feature branches. The default release branch is master, but you could set it to ["stable"] or ["master", "beta", "alpha"] or ["master", "next"]. All branches that are not release branches are considered to be feature branches.

Consider you have the following release branches - master and next, and the following feature branches - fix-bug, improve-search. After executing bap release in each branch, the structure that bap would create on s3 would look something like this:

  bap-bucket
    app-name
      master
        current # file containing 2016-05-06T225500-commit4
        releases
          2016-04-05T225500-commit1
          2016-05-06T225500-commit4
        uploads
          2016-04-05T225500-commit1
          2016-05-06T225500-commit3
          2016-05-06T225500-commit4
      next
        current # file containing 2016-05-17T225500-commit7
        releases
          2016-04-15T225500-commit3
          2016-05-15T225500-commit7
          2016-05-17T225500-commit8
        uploads
          2016-04-15T225500-commit3
          2016-05-15T225500-commit5
          2016-05-15T225500-commit7
          2016-05-17T225500-commit8
          2016-05-17T225500-commit9
      fix-bug
        current # file containing 2016-05-18T225500-commit10
        releases
          2016-05-18T225500-commit10
      improve-search
        current # file containing 2016-05-18T225500-commit12
        releases
          2016-05-18T225500-commit12

Bap has 3 commands for deploying upload, release and rollback.

  • upload - runs npm run build, uploads the contents of the project to s3. For feature branches, it directly uploads the release to releases directory, immediately updates the current pointer and cleans up. For release branches, it only uploads the code to uploads directory and doesn't touch the releases dir.
  • release - updates the current pointer to the latest commit of the current branch. In case the commit in question is not uploaded to s3, upload is executed first. Typically, if you're previously executed the upload task, release will be very quick as it only copies the right release from uploads into releases and updates the current pointer without having to run the build again.
  • rollback - updates current pointer to point to the previous release. Use --forward to update it to the next release. Or specify the release id as an argument to roll to a specific release.

Bap has some helper commands for performing chores on s3 list and remove.

  • list - lists all deployments of all branches
  • remove - removes a deployment specified by a commit or deployment directory. Bap will refuse to remove releases that are pointed at by current.

Build

By default, the build command is npm run build, but you can specify a custom command in package.json key bap.build. Similarly, at the end of the upload or release execution, npm run clean gets run. Change this with bap.clean. Environment variables BAP_RELEASE_ID and BAP_BRANCH are set before executing the build command so that the build process could take this into account. For example, this might be useful when specifying the webpack publicPath option. BAP_RELEASE_ID is of structure YYYY-MM-DDTHHmmss-commit, where commit is the first 7 commit characters.

REVISION

When an upload is succesfully completed, a REVISION file gets created in the release dir. You can check for this file to know if the upload is complete.

Private / Public

Clientside assets are usually meant to be public, say if you want to serve them via Cloudfront CDN. Bapistrano by default makes all uploaded files public.

If you prefer keeping all assets private, use:

"private": ["*"]

If you want to exlude specific files from being public, use:

"private": ["*.js.map", "manifest.json"]

Pattern matching is done using minimatch with matchBase: true.

Embedding bapistrano assets

Once the assets are released to s3, the recommended way to embed them into your html is by using Cloudfront CDN in front of the s3 bucket. Bapistrano comes with a few helpers for retrieving metadata about the releases. E.g.

var bapistrano = require('bapistrano')

var bap = bapistrano.meta({
  bucket: 'ui',
  accessKeyId: '...',
  secretAccessKey: '...',
  cache: 60 * 1000
})

module.exports = async function getBundleUrl () {
  var branch = 'master'
  var releaseId = await bap.getCurrent('my-app', branch)
  return `http://d111111abcdef8.cloudfront.net/my-app/${branch}/${releaseId}/bundle.js`
}

bapistrano.meta(options)

Creates a meta service that can retrieve current release id and a list of branches.

bapistrano.meta({
  bucket: 'ui',
  region: 'eu-west-1',
  accessKeyId: '...',
  secretAccessKey: '...',
  cache: 60 * 1000 // how long to cache the values for before refetching from s3
})

meta.getCurrent(path, branchName)

Get the current release id for the given branch. Returns a release id such as 2016-04-05T225500-commit1.

meta.getCurrent('my-app', 'master')
meta.getCurrent('some/subpath/my-app', 'next') // note that first argument is path corresponding to uploadTo setting used when uploading the assets

Returns null if the branch does not exist.

meta.getBranches(path)

Get a list of available branches.

meta.getBranches('my-app')
meta.getBranches('some/subpath/my-app')

Returns a list of branches such as:

[{
  name: 'master',
  current: '2016-04-05T225500-commit1',
  released: '2016-04-07T10:02:15.000Z',
  uploaded: '2016-04-05T22:55:00.000Z'
}]

Convention vs Configuration

Bapistrano is designed to be easily specialised for your projects without having to repeat the configuration in all of the projects. Say you have 5 projects that you want to deploy with bapistrano and they all live in the same s3 bucket with the same structure and have the same lifecycle commands.

You can create your own tiny npm package with bin/bap that creates a version of bap with custom defaults, e.g.:

#!/usr/bin/env node

var path = require('path')
var meta = require(path.join(process.cwd(), 'package.json'))

require('bapistrano/lib/bin')(Object.assign(require('bapistrano/lib/defaults')(), {
  bucket: 'all-my-apps',
  region: 'eu-west-1',
  uploadTo: 'deployments/' + meta.name,
  build: 'make build',
  clean: 'make clean',
  notify: 'make post-to-slack && curl -X POST https://myapp.com'
}, meta.bap))

Now all 5 of those projects can just call bap upload or bap release without any further configuration in package.json

Contributing

Right now there are no automated tests other than linting.

Run linting with

npm test

Run the manual test with

cd tests/manual
npm run test-cli