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.
$ npm install --save-dev bapistrano
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.
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
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"
]
}
}
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
- runsnpm run build
, uploads the contents of the project to s3. For feature branches, it directly uploads the release toreleases
directory, immediately updates thecurrent
pointer and cleans up. For release branches, it only uploads the code touploads
directory and doesn't touch thereleases
dir.release
- updates thecurrent
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 theupload
task, release will be very quick as it only copies the right release fromuploads
intoreleases
and updates thecurrent
pointer without having to run the build again.rollback
- updatescurrent
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 branchesremove
- removes a deployment specified by a commit or deployment directory. Bap will refuse to remove releases that are pointed at bycurrent
.
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.
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.
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
.
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`
}
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
})
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.
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'
}]
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
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