Skip to content
This repository has been archived by the owner on Jul 26, 2021. It is now read-only.

Commit

Permalink
Add tests, update readme, consolidate host + bucket into domain, bump…
Browse files Browse the repository at this point in the history
… to version 2
  • Loading branch information
notVitaliy committed May 28, 2019
1 parent 12243ec commit dee4169
Show file tree
Hide file tree
Showing 14 changed files with 403 additions and 136 deletions.
66 changes: 31 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,74 +1,82 @@
# s3-deploy

A command line tool to deploy static assets to an S3 bucket.
A command line tool to deploy static sites to an S3 bucket.

## Why?

At [Moneylion](https://moneylion.com), we have a lot of web properties. In order to untangle some of our deploy processes for frontend assets, we developed this script which allows us to quickly and painlessly configure a new deployment to S3.

One of the main uses for this script is to create temporary review-apps. When a new PR is created, this triggers a build in our CI pipeline, if all the tests pass and it builds successfully, then we deploy that PR to a temporary and shareable URL. Once the PR is closed, we tear it down.

## Required ENV Variables

- NPM_TOKEN
- AWS_SECRET_ACCESS_KEY
- AWS_ACCESS_KEY_ID
- AWS_DEFAULT_REGION

### optional

- CODEFRESH_SLACK_BOT_TOKEN
- SLACK_TOKEN

## Usage

`npx @moneylion/s3-deploy`

## Arguments

| argument | description |
| -------------- | ------------------------------ |
| `domain` | A fully qualified domain name. |
| `zone` | The route53 HostedZoneId. |
| `distribution` | The CloudFront DistributionId. |
| `channel` | The slack channel name. |

## Commands

### `deploy`

This will create a new S3 bucket and point route53 at it. The last argument must be the directoy to be uploaded.
This will create a new S3 bucket and point route53 at it. The last argument must be the directory to be uploaded. If an S3 bucket with this name already exists, then it will be cleared before the new files are uploaded.

requires:

- host
- bucket
- domain
- zone

e.g.<br>
`npx @moneylion/s3-deploy deploy --host example.com --bucket test --zone Z2XDC2IJ26IK32 ./dist`
`npx @moneylion/s3-deploy deploy --domain test.example.com --zone Z2XDC2IJ26IK32 ./dist`

### `undeploy`

This will delete an S3 bucket and the route53 record set.

requires:

- host
- bucket
- domain
- zone

optional

- channel

e.g.<br>
`npx @moneylion/s3-deploy undeploy --host example.com --bucket test --zone Z2XDC2IJ26IK32`
`npx @moneylion/s3-deploy undeploy --domain example.com --zone Z2XDC2IJ26IK32`

### `promote`

_Creating a cloudfront distribution is outside the scope of this script. There are no future plans to support this feature._

> requires an already existing bucket <br>
> requires an already existing cloudfront distribution
This will not create a new bucket, it will instead empty it and upload the new assets to that bucket. It will then invalidate the cloudfront cache.
This will not create a new bucket, it will instead empty an already existing and then upload the new assets to that bucket. It will also invalidate the cloudfront cache.

requires:

- host
- bucket
- domain
- distribution

e.g.<br>
`npx @moneylion/s3-deploy promote --host example.com --bucket test --distribution EINBTGEF4J77C ./dist`

---

Bucket names are constructed by concatenating the `host` and `bucket` strings. The last argument must be the directoy to be uploaded.

bucket = test <br>
host = example.com

Will give you `test.example.com` as the bucket name.
`npx @moneylion/s3-deploy promote --domain test.example.com --distribution ABCDEFGH1I23J ./dist`

---

Expand All @@ -78,18 +86,6 @@ Both `deploy` and `promote` can take in a `channel` argument, this will be the s

## Troubleshooting

### Getting 404's from npm

This is a private script and it reads from `.npmrc` for the auth token.

A quick & dirty workaround for CI or Docker is to create a local `.npmrc` before running the script.

`echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > $(pwd)/.npmrc`

Then run it with the `--userconfig` flag.

`npx --userconfig $(pwd)/.npmrc @moneylion/s3-deploy`

### Undefined environment variables

It might be necessary to inject the environment variable directly into the script.
Expand Down
20 changes: 8 additions & 12 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
module.exports = {
verbose: true,
transform: {
"^.+\\.ts$": "ts-jest"
},
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$",
moduleDirectories: ["node_modules", "app"],
moduleFileExtensions: ["ts", "js", "json", "node"],
moduleNameMapper: {
"\\.(css|eot|woff|woff2)$": "<rootDir>/app/spec/__mocks__/styleMock.js",
"util/jss": "<rootDir>/app/spec/__mocks__/jssMock.js"
'^.+\\.ts$': 'ts-jest'
},
testRegex: 'test\\.ts$',
moduleDirectories: ['node_modules', 'app'],
moduleFileExtensions: ['ts', 'js', 'json', 'node'],
globals: {
"ts-jest": {
tsConfigFile: "tsconfig.json"
'ts-jest': {
tsConfigFile: 'tsconfig.json'
}
},
roots: ["<rootDir>/app"],
roots: ['<rootDir>/src'],
collectCoverage: true,
coveragePathIgnorePatterns: ["/node_modules"]
coveragePathIgnorePatterns: ['/node_modules']
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@moneylion/s3-deploy",
"version": "1.3.1",
"version": "2.0.0",
"engines": {
"node": ">=11.13.0",
"yarn": ">=1.15.0"
},
"main": "./dist/ops.js",
"bin": "./dist/ops.js",
"main": "./dist/index.js",
"bin": "./dist/index.js",
"scripts": {
"lint": "tslint --project ./tsconfig.json",
"lint:fix": "tslint --fix --project ./tsconfig.json",
Expand Down
129 changes: 129 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env node

import mri from 'mri'

interface ParsedArgs {
action: 'deploy' | 'undeploy' | 'promote'
domain: string

zone?: string
distribution?: string
channel?: string
dir?: string
}

const errors = {
domain: {
req: 'Domain is required',
char: "Domain can only be a-z, 0-9, '.', and '-'."
},
zone: {
req: 'Zone is required',
char: 'Zone can only be A-Z and 0-9.',
len: 'Zone has to be 14 characters long.'
},
directory: { req: 'Directory is required' },
distribution: { req: 'Distribution is required' }
}

const parseArgs = (argv: string[]): ParsedArgs => {
const args = mri(argv, { string: ['domain', 'zone', 'channel'] })

const {
_: [action, dir],
domain,
zone,
distribution,
channel
} = args

return { action, dir, domain, zone, channel, distribution } as ParsedArgs
}

const forcedExit = (msg: string) => {
console.error(msg)
process.exit(1)
}

const isValidAction = (action: string) => {
const allowedActions = new Set(['promote', 'deploy', 'undeploy'])

if (!allowedActions.has(action))
return `${action} is not a valid action. Allowed actions: ${Array.from(allowedActions.values()).join(', ')}`

return true
}

const isValidDomain = (domain: string) => {
if (typeof domain === 'undefined') return errors.domain.req
if (!/^[a-z0-9-\.]+$/.test(domain)) return errors.domain.char

return true
}

const isValidZone = (zone: string) => {
if (typeof zone === 'undefined') return errors.zone.req
if (!/^[A-Z0-9]+$/.test(zone)) return errors.zone.char
if (zone.length !== 14) return errors.zone.len

return true
}

const isValidDir = (dir: string) => {
if (typeof dir === 'undefined') return errors.directory.req

return true
}

const isValidDistribution = (distribution: string) => {
if (typeof distribution === 'undefined') return errors.distribution.req

return true
}

const deploy = ({ domain, zone, dir, channel }: ParsedArgs) => {
const validDomain = isValidDomain(domain)
const validZone = isValidZone(zone)
const validDir = isValidDir(dir)

if (validDomain !== true) return forcedExit(validDomain)
if (validZone !== true) return forcedExit(validZone)
if (validDir !== true) return forcedExit(validDir)

require('./deploy').deploy(domain, zone, dir, channel)
}

const undeploy = ({ domain, zone }: ParsedArgs) => {
const validDomain = isValidDomain(domain)
const validZone = isValidZone(zone)

if (validDomain !== true) return forcedExit(validDomain)
if (validZone !== true) return forcedExit(validZone)

require('./undeploy').undeploy(domain, zone)
}

const promote = ({ domain, distribution, dir, channel }: ParsedArgs) => {
const validDomain = isValidDomain(domain)
const validDistribution = isValidDistribution(distribution)
const validDir = isValidDir(dir)

if (validDomain !== true) return forcedExit(validDomain)
if (validDistribution !== true) return forcedExit(validDistribution)
if (validDir !== true) return forcedExit(validDir)

require('./promote').promote(domain, distribution, dir, channel)
}

export const cli = (argv: string[]) => {
const args = parseArgs(argv)

const { action } = args

const validAction = isValidAction(action)
if (validAction !== true) return forcedExit(validAction)

if (action === 'deploy') deploy(args)
else if (action === 'undeploy') undeploy(args)
else if (action === 'promote') promote(args)
}
Loading

0 comments on commit dee4169

Please sign in to comment.