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

Commit dee4169

Browse files
committed
Add tests, update readme, consolidate host + bucket into domain, bump to version 2
1 parent 12243ec commit dee4169

File tree

14 files changed

+403
-136
lines changed

14 files changed

+403
-136
lines changed

README.md

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,82 @@
11
# s3-deploy
22

3-
A command line tool to deploy static assets to an S3 bucket.
3+
A command line tool to deploy static sites to an S3 bucket.
4+
5+
## Why?
6+
7+
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.
8+
9+
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.
410

511
## Required ENV Variables
612

7-
- NPM_TOKEN
813
- AWS_SECRET_ACCESS_KEY
914
- AWS_ACCESS_KEY_ID
1015
- AWS_DEFAULT_REGION
1116

1217
### optional
1318

14-
- CODEFRESH_SLACK_BOT_TOKEN
19+
- SLACK_TOKEN
1520

1621
## Usage
1722

1823
`npx @moneylion/s3-deploy`
1924

25+
## Arguments
26+
27+
| argument | description |
28+
| -------------- | ------------------------------ |
29+
| `domain` | A fully qualified domain name. |
30+
| `zone` | The route53 HostedZoneId. |
31+
| `distribution` | The CloudFront DistributionId. |
32+
| `channel` | The slack channel name. |
33+
2034
## Commands
2135

2236
### `deploy`
2337

24-
This will create a new S3 bucket and point route53 at it. The last argument must be the directoy to be uploaded.
38+
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.
2539

2640
requires:
2741

28-
- host
29-
- bucket
42+
- domain
3043
- zone
3144

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

3548
### `undeploy`
3649

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

3952
requires:
4053

41-
- host
42-
- bucket
54+
- domain
4355
- zone
4456

57+
optional
58+
59+
- channel
60+
4561
e.g.<br>
46-
`npx @moneylion/s3-deploy undeploy --host example.com --bucket test --zone Z2XDC2IJ26IK32`
62+
`npx @moneylion/s3-deploy undeploy --domain example.com --zone Z2XDC2IJ26IK32`
4763

4864
### `promote`
4965

66+
_Creating a cloudfront distribution is outside the scope of this script. There are no future plans to support this feature._
67+
5068
> requires an already existing bucket <br>
5169
> requires an already existing cloudfront distribution
5270
53-
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.
71+
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.
5472

5573
requires:
5674

57-
- host
58-
- bucket
75+
- domain
5976
- distribution
6077

6178
e.g.<br>
62-
`npx @moneylion/s3-deploy promote --host example.com --bucket test --distribution EINBTGEF4J77C ./dist`
63-
64-
---
65-
66-
Bucket names are constructed by concatenating the `host` and `bucket` strings. The last argument must be the directoy to be uploaded.
67-
68-
bucket = test <br>
69-
host = example.com
70-
71-
Will give you `test.example.com` as the bucket name.
79+
`npx @moneylion/s3-deploy promote --domain test.example.com --distribution ABCDEFGH1I23J ./dist`
7280

7381
---
7482

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

7987
## Troubleshooting
8088

81-
### Getting 404's from npm
82-
83-
This is a private script and it reads from `.npmrc` for the auth token.
84-
85-
A quick & dirty workaround for CI or Docker is to create a local `.npmrc` before running the script.
86-
87-
`echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > $(pwd)/.npmrc`
88-
89-
Then run it with the `--userconfig` flag.
90-
91-
`npx --userconfig $(pwd)/.npmrc @moneylion/s3-deploy`
92-
9389
### Undefined environment variables
9490

9591
It might be necessary to inject the environment variable directly into the script.

jest.config.js

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
module.exports = {
22
verbose: true,
33
transform: {
4-
"^.+\\.ts$": "ts-jest"
5-
},
6-
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts)$",
7-
moduleDirectories: ["node_modules", "app"],
8-
moduleFileExtensions: ["ts", "js", "json", "node"],
9-
moduleNameMapper: {
10-
"\\.(css|eot|woff|woff2)$": "<rootDir>/app/spec/__mocks__/styleMock.js",
11-
"util/jss": "<rootDir>/app/spec/__mocks__/jssMock.js"
4+
'^.+\\.ts$': 'ts-jest'
125
},
6+
testRegex: 'test\\.ts$',
7+
moduleDirectories: ['node_modules', 'app'],
8+
moduleFileExtensions: ['ts', 'js', 'json', 'node'],
139
globals: {
14-
"ts-jest": {
15-
tsConfigFile: "tsconfig.json"
10+
'ts-jest': {
11+
tsConfigFile: 'tsconfig.json'
1612
}
1713
},
18-
roots: ["<rootDir>/app"],
14+
roots: ['<rootDir>/src'],
1915
collectCoverage: true,
20-
coveragePathIgnorePatterns: ["/node_modules"]
16+
coveragePathIgnorePatterns: ['/node_modules']
2117
}

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"name": "@moneylion/s3-deploy",
3-
"version": "1.3.1",
3+
"version": "2.0.0",
44
"engines": {
55
"node": ">=11.13.0",
66
"yarn": ">=1.15.0"
77
},
8-
"main": "./dist/ops.js",
9-
"bin": "./dist/ops.js",
8+
"main": "./dist/index.js",
9+
"bin": "./dist/index.js",
1010
"scripts": {
1111
"lint": "tslint --project ./tsconfig.json",
1212
"lint:fix": "tslint --fix --project ./tsconfig.json",

src/cli.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env node
2+
3+
import mri from 'mri'
4+
5+
interface ParsedArgs {
6+
action: 'deploy' | 'undeploy' | 'promote'
7+
domain: string
8+
9+
zone?: string
10+
distribution?: string
11+
channel?: string
12+
dir?: string
13+
}
14+
15+
const errors = {
16+
domain: {
17+
req: 'Domain is required',
18+
char: "Domain can only be a-z, 0-9, '.', and '-'."
19+
},
20+
zone: {
21+
req: 'Zone is required',
22+
char: 'Zone can only be A-Z and 0-9.',
23+
len: 'Zone has to be 14 characters long.'
24+
},
25+
directory: { req: 'Directory is required' },
26+
distribution: { req: 'Distribution is required' }
27+
}
28+
29+
const parseArgs = (argv: string[]): ParsedArgs => {
30+
const args = mri(argv, { string: ['domain', 'zone', 'channel'] })
31+
32+
const {
33+
_: [action, dir],
34+
domain,
35+
zone,
36+
distribution,
37+
channel
38+
} = args
39+
40+
return { action, dir, domain, zone, channel, distribution } as ParsedArgs
41+
}
42+
43+
const forcedExit = (msg: string) => {
44+
console.error(msg)
45+
process.exit(1)
46+
}
47+
48+
const isValidAction = (action: string) => {
49+
const allowedActions = new Set(['promote', 'deploy', 'undeploy'])
50+
51+
if (!allowedActions.has(action))
52+
return `${action} is not a valid action. Allowed actions: ${Array.from(allowedActions.values()).join(', ')}`
53+
54+
return true
55+
}
56+
57+
const isValidDomain = (domain: string) => {
58+
if (typeof domain === 'undefined') return errors.domain.req
59+
if (!/^[a-z0-9-\.]+$/.test(domain)) return errors.domain.char
60+
61+
return true
62+
}
63+
64+
const isValidZone = (zone: string) => {
65+
if (typeof zone === 'undefined') return errors.zone.req
66+
if (!/^[A-Z0-9]+$/.test(zone)) return errors.zone.char
67+
if (zone.length !== 14) return errors.zone.len
68+
69+
return true
70+
}
71+
72+
const isValidDir = (dir: string) => {
73+
if (typeof dir === 'undefined') return errors.directory.req
74+
75+
return true
76+
}
77+
78+
const isValidDistribution = (distribution: string) => {
79+
if (typeof distribution === 'undefined') return errors.distribution.req
80+
81+
return true
82+
}
83+
84+
const deploy = ({ domain, zone, dir, channel }: ParsedArgs) => {
85+
const validDomain = isValidDomain(domain)
86+
const validZone = isValidZone(zone)
87+
const validDir = isValidDir(dir)
88+
89+
if (validDomain !== true) return forcedExit(validDomain)
90+
if (validZone !== true) return forcedExit(validZone)
91+
if (validDir !== true) return forcedExit(validDir)
92+
93+
require('./deploy').deploy(domain, zone, dir, channel)
94+
}
95+
96+
const undeploy = ({ domain, zone }: ParsedArgs) => {
97+
const validDomain = isValidDomain(domain)
98+
const validZone = isValidZone(zone)
99+
100+
if (validDomain !== true) return forcedExit(validDomain)
101+
if (validZone !== true) return forcedExit(validZone)
102+
103+
require('./undeploy').undeploy(domain, zone)
104+
}
105+
106+
const promote = ({ domain, distribution, dir, channel }: ParsedArgs) => {
107+
const validDomain = isValidDomain(domain)
108+
const validDistribution = isValidDistribution(distribution)
109+
const validDir = isValidDir(dir)
110+
111+
if (validDomain !== true) return forcedExit(validDomain)
112+
if (validDistribution !== true) return forcedExit(validDistribution)
113+
if (validDir !== true) return forcedExit(validDir)
114+
115+
require('./promote').promote(domain, distribution, dir, channel)
116+
}
117+
118+
export const cli = (argv: string[]) => {
119+
const args = parseArgs(argv)
120+
121+
const { action } = args
122+
123+
const validAction = isValidAction(action)
124+
if (validAction !== true) return forcedExit(validAction)
125+
126+
if (action === 'deploy') deploy(args)
127+
else if (action === 'undeploy') undeploy(args)
128+
else if (action === 'promote') promote(args)
129+
}

0 commit comments

Comments
 (0)