diff --git a/.circleci/config.yml b/.circleci/config.yml index 13ec1b3..daa1738 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -39,10 +39,7 @@ jobs: - run: name: Deploy to S3 - command: | - sudo apt-get -y -qq install awscli - aws s3 sync build s3://$DEPLOY_BUCKET/ --delete - aws s3api copy-object --copy-source $DEPLOY_BUCKET/service-worker.js --bucket $DEPLOY_BUCKET --key service-worker.js --metadata-directive REPLACE --cache-control "max-age=0" --content-type "application/javascript" + command: yarn deploy workflows: version: 2 diff --git a/bucket.config.js b/bucket.config.js new file mode 100644 index 0000000..d084cf1 --- /dev/null +++ b/bucket.config.js @@ -0,0 +1,20 @@ +const path = require('path'); + +module.exports = { + bucket: process.env.DEPLOY_BUCKET, + deployFrom: path.resolve(__dirname, 'build'), + paths: [ + { + match: /^service-worker.js/, + CacheControl: 'max-age=0' + }, + { + match: /^(js|css|icons-\w{8})/, + CacheControl: 'public, max-age=31536000' + }, + { // Default config. MUST BE LAST + match: /./, + CacheControl: 'public, max-age=96400' + } + ] +}; diff --git a/package.json b/package.json index 8c2adb7..d369c5c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "build:webpack:web": "webpack --config webpack.prod-web.js", "build:webpack:prerender": "webpack --config webpack.prod-prerender.js", "build:prerender": "node ./script/__build__/prerender.js", + "deploy": "node ./script/s3-upload.js bucket.config.js", "test": "run-s test:lint 'test:unit --coverage'", "test:unit": "cross-env NODE_ENV=production jest", "test:lint": "eslint --ignore-path .gitignore .", @@ -118,6 +119,7 @@ }, "dependencies": { "@alienfast/i18next-loader": "^1.0.14", + "aws-sdk": "^2.247.1", "babel-core": "^6.26.0", "babel-eslint": "^8.2.1", "babel-jest": "^22.2.2", @@ -149,6 +151,7 @@ "identity-obj-proxy": "^3.0.0", "immutable": "^3.8.2", "jest": "^22.2.2", + "mime-types": "^2.1.18", "npm-run-all": "^4.1.2", "postcss-cssnext": "^3.1.0", "postcss-import": "^11.1.0", @@ -159,6 +162,7 @@ "react-ga": "^2.4.1", "react-i18next": "^7.3.6", "react-test-renderer": "^16.3.0", + "recursive-readdir": "^2.2.2", "style-loader": "^0.21.0", "svg-react-loader": "^0.4.5", "uglifyjs-webpack-plugin": "^1.1.8", diff --git a/script/s3-upload.js b/script/s3-upload.js new file mode 100644 index 0000000..cec20db --- /dev/null +++ b/script/s3-upload.js @@ -0,0 +1,80 @@ +/* eslint-disable no-console */ + +const path = require('path'); +const fs = require('fs'); +const colors = require('colors/safe'); +const readdir = require('recursive-readdir'); +const AWS = require('aws-sdk'); +const mime = require('mime-types'); + +const s3 = new AWS.S3(); +const config = require(path.resolve(process.argv[2])); + +const configFor = path => { + const { match, ...conf } = config.paths.find(conf => conf.match.test(path)); // eslint-disable-line no-unused-vars + return { + ContentEncoding: mime.lookup(path) || 'application/octet-stream', + ...conf + }; +}; + +const bucketContents = s3.listObjectsV2({ + Bucket: config.bucket +}).promise() + .then(result => { + return result.Contents.map(item => item.Key); + }) + .catch(err => { + console.error(colors.red.bold('Failed to fetch bucket contents:'), err); + process.exit(1); + }); + +const uploadDetails = readdir(config.deployFrom) + .then(paths => paths.map(p => { + const key = path.relative(config.deployFrom, p); + return { + Key: key, + Body: fs.createReadStream(p), + ...configFor(key) + }; + })) + .catch(err => { + console.error(colors.red.bold('Error:'), err); + process.exit(1); + }); + +Promise.all([bucketContents, uploadDetails]).then(([bucket, upload]) => { + const deleteKeys = bucket.filter(key => !upload.find(conf => key === conf.Key)); + + const uploadPromises = upload.map(params => { + console.log(`Starting upload for ${ params.Key }`); + return s3.upload({ + Bucket: config.bucket, + ...params + }).promise() + .then(() => console.log(colors.green(`${ params.Key } successful`))) + .catch(err => { + console.error(colors.red.bold(`${ params.Key } failed`)); + return Promise.reject(err); + }); + }); + + return Promise.all(uploadPromises).then(() => { + console.log(`Deleting ${ deleteKeys.length } stale files`); + return s3.deleteObjects({ + Bucket: config.bucket, + Delete: { + Objects: deleteKeys.map(key => ({ Key: key })) + } + }).promise() + .then(() => console.log(colors.green('Delete successful'))) + .catch(err => { + console.error(colors.red.bold('Delete failed')); + return Promise.reject(err); + }); + }); +}) + .catch(err => { + console.error(colors.red.bold('Error:'), err); + process.exit(1); + }); diff --git a/yarn.lock b/yarn.lock index bfeb19e..2667ad6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -582,6 +582,20 @@ autoprefixer@^7.1.1: postcss "^6.0.17" postcss-value-parser "^3.2.3" +aws-sdk@^2.247.1: + version "2.247.1" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.247.1.tgz#be5f220d40665ac91d3a84a51f029fa05560c4ee" + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.8" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.1.0" + xml2js "0.4.17" + aws-sign2@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" @@ -1647,7 +1661,7 @@ buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" -buffer@^4.3.0: +buffer@4.9.1, buffer@^4.3.0: version "4.9.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" dependencies: @@ -3197,7 +3211,7 @@ eventemitter3@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" -events@^1.0.0: +events@1.1.1, events@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -4402,6 +4416,10 @@ identity-obj-proxy@^3.0.0: dependencies: harmony-reflect "^1.4.6" +ieee754@1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + ieee754@^1.1.4, ieee754@^1.1.8: version "1.1.11" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455" @@ -5268,6 +5286,10 @@ jimp@^0.2.21, jimp@^0.2.28: tinycolor2 "^1.1.2" url-regex "^3.0.0" +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + joi@^11.1.1: version "11.4.0" resolved "https://registry.yarnpkg.com/joi/-/joi-11.4.0.tgz#f674897537b625e9ac3d0b7e1604c828ad913ccb" @@ -5949,7 +5971,7 @@ miller-rabin@^4.0.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7: +mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7: version "2.1.18" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" dependencies: @@ -5989,7 +6011,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -7863,6 +7885,12 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" +recursive-readdir@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" + dependencies: + minimatch "3.0.4" + redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -8291,6 +8319,10 @@ sane@^2.0.0: optionalDependencies: fsevents "^1.1.1" +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + sax@>=0.6.0, sax@^1.2.4, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -9348,6 +9380,13 @@ url-to-options@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9" +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -9390,6 +9429,10 @@ utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" +uuid@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0: version "3.2.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"