Skip to content

Commit

Permalink
Migrate deployment method from the blog repo (#1105)
Browse files Browse the repository at this point in the history
* Migrate deployment process to s3 from the blog

* Add trailing slash redirect to redirects list

* Allow all node versions great or equal than 12.0.0

* Migrate cloudflare worker redirects to redirects-list.json

* Allow second redirect to remove trailing slash. Add tests for new redirects

* Exclude redirects-list.json file from links checker

Co-authored-by: Pavel Grinchenko <[email protected]>
  • Loading branch information
pavelgrinchenko and psdcoder authored Apr 2, 2020
1 parent bb36d06 commit 847d4ad
Show file tree
Hide file tree
Showing 22 changed files with 529 additions and 95 deletions.
4 changes: 3 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
{
"files": [
"src/utils/**/*.?(js|ts)",
"+(middleware|scripts)/**/*.+(js|ts)"
"scripts/**/*.js",
"src/server/**/*.js",
"gatsby-*.js"
],
"rules": {
"@typescript-eslint/no-var-requires": "off"
Expand Down
4 changes: 2 additions & 2 deletions gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ const path = require('path')
require('./config/prismjs/dvc')
require('./config/prismjs/usage')

const apiMiddleware = require('./middleware/api')
const redirectsMiddleware = require('./middleware/redirects')
const apiMiddleware = require('./src/server/middleware/api')
const redirectsMiddleware = require('./src/server/middleware/redirects')
const { BLOG } = require('./src/consts')

const title = 'Data Version Control · DVC'
Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
"develop": "gatsby develop",
"debug": "node --inspect-brk server.js",
"build": "gatsby build",
"heroku-postbuild": "./scripts/deploy-with-s3.js",
"test": "jest",
"start": "./scripts/clear-cloudflare-cache.js; NODE_ENV=production node server.js",
"start": "./scripts/clear-cloudflare-cache.js; node ./src/server/index.js",
"format-staged": "pretty-quick --staged --no-restage --bail",
"format-check": "prettier --check '{.,pages/**,content/docs/**,src/**}/*.{js,md,json}'",
"format-all": "prettier --write './**/*.{js,jsx,md,tsx,ts,json}'",
"lint-ts": "tsc --noEmit --skipLibCheck && eslint --ext .json,.js,.ts,.tsx src middleware scripts",
"lint-ts": "tsc --noEmit --skipLibCheck && eslint --ext .json,.js,.ts,.tsx src scripts",
"lint-css": "stylelint \"src/**/*.css\"",
"link-check": "scripts/link-check-git-all.sh",
"link-check-diff": "scripts/link-check-git-diff.sh"
Expand All @@ -28,9 +29,10 @@
},
"homepage": "https://github.com/iterative/dvc.org#readme",
"engines": {
"node": "^12.0.0"
"node": ">=12.0.0"
},
"dependencies": {
"@hapi/wreck": "^17.0.0",
"@octokit/graphql": "^4.3.1",
"@reach/portal": "^0.9.0",
"@reach/router": "^1.3.1",
Expand All @@ -43,6 +45,7 @@
"date-fns": "^2.8.1",
"docsearch.js": "^2.6.3",
"express": "^4.17.1",
"fs-extra": "^9.0.0",
"gatsby": "^2.20.2",
"gatsby-image": "^2.3.0",
"gatsby-link": "^2.3.0",
Expand All @@ -57,7 +60,6 @@
"lodash.startcase": "^4.4.0",
"lodash.throttle": "^4.1.1",
"lodash.topairs": "^4.3.0",
"micro-cors": "^0.1.1",
"node-cache": "^5.1.0",
"perfect-scrollbar": "^1.4.0",
"prismjs": "^1.19.0",
Expand All @@ -76,6 +78,7 @@
"react-use": "^13.27.0",
"rehype-react": "^4.0.1",
"request": "^2.88.0",
"s3-client": "^4.4.2",
"serve-handler": "^6.1.2",
"slick-carousel": "^1.8.1",
"styled-components": "^4.4.1",
Expand Down
46 changes: 27 additions & 19 deletions redirects-list.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
[
"^https://blog\\.dvc\\.org/blog/(.*?)/?$ https://dvc.org/blog/$1",
"^https://blog\\.dvc\\.org/(.*?)/?$ https://dvc.org/blog/$1",
"^https://(?:www\\.)dvc\\.org/(.+)? https://dvc.org/$1",
"^https://man\\.dvc\\.org/(.+)? https://dvc.org/doc/command-reference/$1 303",
"^https://error\\.dvc\\.org/(.+)? https://dvc.org/doc/user-guide/troubleshooting#$1 303",
"^https://(code|data|remote)\\.dvc\\.org/(.+) https://s3-us-east-2.amazonaws.com/dvc-public/$1/$2 303",
"^/((?:deb|rpm)/.+) https://s3-us-east-2.amazonaws.com/dvc-s3-repo/$1 303",
"^/(?:help|chat)/?$ https://discordapp.com/invite/dvwXA2N 303",
"^/(?:docs|documentation)(/.*)?$ /doc$1",
"^/doc/?$ /doc/home 307",
"^/doc/get-started(/.*)?$ /doc/tutorials/get-started$1",
"^/doc/tutorials/get-started/?$ /doc/tutorials/get-started/agenda 307",
"^/doc/tutorial/?$ /doc/tutorials",
"^/doc/tutorial/(.*)? /doc/tutorials/deep/$1",
"^/doc/commands-reference(/.*)?$ /doc/command-reference$1",
"^/doc/use-cases/data-and-model-files-versioning/?$ /doc/use-cases/versioning-data-and-model-files",
"^/doc/user-guide/contributing/?$ /doc/user-guide/contributing/core 307",
"^/doc/understanding-dvc/?$ /doc/understanding-dvc/collaboration-issues 307",
"^/doc/changelog/?$ /doc/changelog/0.18 307"
"^https://(?:www\\.)dataversioncontrol\\.com(.*)? https://dataversioncontrol.com$1 301",
"^https://blog\\.dataversioncontrol\\.com/data-version-control-tutorial-9146715eda46 https://dvc.org/doc/tutorials 301",
"^https://blog\\.dataversioncontrol\\.com/data-version-control-beta-release-iterative-machine-learning-a7faf7c8be67 https://dvc.org/doc/tutorials 301",
"^https://blog\\.dataversioncontrol\\.com/dvc-heartbeat-6301aebf5c96 https://dvc.org/blog/march-19-dvc-heartbeat 301",
"^https://blog\\.dataversioncontrol\\.com/april19-dvc-heartbeat-296c71a59be4 https://dvc.org/blog/april-19-dvc-heartbeat 301",
"^https://blog\\.dataversioncontrol\\.com/dvc-0-8-5-release-f66ef3b10684 https://github.com/iterative/dvc/releases 301",
"^https://blog\\.dataversioncontrol\\.com(.+)-[a-z0-9]{12}$ https://dvc.org/blog$1 301",
"^https://blog\\.dvc\\.org/blog/(.*?)/?$ https://dvc.org/blog/$1",
"^https://blog\\.dvc\\.org/(.*?)/?$ https://dvc.org/blog/$1",
"^https://(?:www\\.)dvc\\.org(.*)? https://dvc.org$1",
"^https://man\\.dvc\\.org(.*)? https://dvc.org/doc/command-reference$1 303",
"^https://error\\.dvc\\.org/(.+)? https://dvc.org/doc/user-guide/troubleshooting#$1 303",
"^https://(code|data|remote)\\.dvc\\.org/(.+) https://s3-us-east-2.amazonaws.com/dvc-public/$1/$2 303",
"^/((?:deb|rpm)/.+) https://s3-us-east-2.amazonaws.com/dvc-s3-repo/$1 303",
"^/(?:help|chat)/?$ https://discordapp.com/invite/dvwXA2N 303",
"^/(?:docs|documentation)(/.*)?$ /doc$1",
"^/doc/?$ /doc/home 307",
"^/doc/get-started(/.*)?$ /doc/tutorials/get-started$1",
"^/doc/tutorials/get-started/?$ /doc/tutorials/get-started/agenda 307",
"^/doc/tutorial/?$ /doc/tutorials",
"^/doc/tutorial/(.*)? /doc/tutorials/deep/$1",
"^/doc/commands-reference(/.*)?$ /doc/command-reference$1",
"^/doc/use-cases/data-and-model-files-versioning/?$ /doc/use-cases/versioning-data-and-model-files",
"^/doc/user-guide/contributing/?$ /doc/user-guide/contributing/core 307",
"^/doc/understanding-dvc/?$ /doc/understanding-dvc/collaboration-issues 307",
"^/doc/changelog/?$ /doc/changelog/0.18 307",
"^/(.+)/$ /$1 301"
]
130 changes: 130 additions & 0 deletions scripts/deploy-with-s3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/usr/bin/env node
'use strict'

/**
* Build gatsby site and deploy public/ to s3.
*
* The S3 path of the deployment depends on the HEROKU_APP_NAME variable,
* which is passed to PRs by heroku, but you can set locally too.
*
* With HEROKU_APP_NAME: /dvc-org-pulls/$HEROKU_APP_NAME
* Without HEROKU_APP_NAME: /dvc-org-prod
*
* Needs following environment variables:
*
* - S3_BUCKET: name of the bucket
* - AWS_REGION: region of the bucket
* - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: auth token to access the bucket.
* - HEROKU_APP_NAME: (optional) app name to specify the ID of the PR if any.
**/

const path = require('path')
const { execSync } = require('child_process')
const { remove, move, ensureDir } = require('fs-extra')
const { s3Prefix, s3Bucket, s3Client } = require('./s3-utils')

const rootDir = path.join(__dirname, '..')
const cacheDir = path.join(rootDir, '.cache')
const publicDir = path.join(rootDir, 'public')

function run(command) {
execSync(command, {
stdio: ['pipe', process.stdout, process.stderr]
})
}

function syncCall(method, ...args) {
return new Promise((resolve, reject) => {
const synchroniser = s3Client[method](...args)
synchroniser.on('error', reject)
synchroniser.on('end', resolve)
})
}

async function prefixIsEmpty(prefix) {
try {
await s3Client.s3
.headObject({
Bucket: s3Bucket,
Prefix: prefix + '/index.html'
})
.promise()
return false
} catch (e) {
return true
}
}

async function downloadFromS3(prefix) {
try {
const staticDir = path.join(publicDir, 'static')
const staticPrefix = prefix + '/static'
await ensureDir(staticDir)

console.log(
`downloading public/static from s3://${s3Bucket}/${staticPrefix}`
)
console.time('download from s3')
await syncCall('downloadDir', {
localDir: staticDir,
s3Params: {
Bucket: s3Bucket,
Prefix: staticPrefix
}
})
console.timeEnd('download from s3')
} catch (downloadError) {
console.error('Error downloading initial data', downloadError)
// Don't propagate. It's just a cache warming step
}
}

async function uploadToS3() {
console.log(`Uploading public/ to s3://${s3Bucket}/${s3Prefix}`)
console.time('upload to s3')
await syncCall('uploadDir', {
localDir: publicDir,
deleteRemoved: true,
s3Params: {
Bucket: s3Bucket,
Prefix: s3Prefix
}
})
console.timeEnd('upload to s3')
}

async function main() {
const emptyPrefix = await prefixIsEmpty(s3Prefix)

// First build of a PR is slow because it can't reuse cache.
// But we can download from prod to warm cache up.
const cacheWarmPrefix = emptyPrefix ? 'dvc-org-prod' : s3Prefix

await downloadFromS3(cacheWarmPrefix)

try {
run('yarn build')
} catch (buildError) {
// Sometimes gatsby build fails because of bad cache.
// Clear it and try again.

console.error('------------------------\n\n')
console.error(buildError)
console.error('\nAssuming bad cache and retrying:\n')

await remove(cacheDir)
await remove(publicDir)
run('yarn build')
}

await move(path.join(publicDir, '404.html'), path.join(rootDir, '404.html'), {
overwrite: true
})
await uploadToS3()
await remove(publicDir)
}

main().catch(e => {
console.error(e)
process.exit(1)
})
2 changes: 1 addition & 1 deletion scripts/link-check-git-diff.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -euo pipefail

exclude="${CHECK_LINKS_EXCLUDE_LIST:-$(dirname $0)/exclude-links.txt}"
differ="git diff $(git merge-base HEAD origin/master)"
changed="$($differ --name-only)"
changed="$differ --name-only :^redirects-list.json"

[ -z "$changed" ] && exit 0

Expand Down
32 changes: 32 additions & 0 deletions scripts/s3-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict'

const { s3Prefix, s3Bucket } = require('../src/server/config')
const s3 = require('s3-client')

const {
AWS_REGION,
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
HEROKU_APP_NAME
} = process.env

const s3Client = s3.createClient({
maxAsyncS3: 50,
region: AWS_REGION,
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY
})

console.log({
AWS_REGION,
HEROKU_APP_NAME,
s3Bucket,
s3Prefix,
hasCreds: Boolean(AWS_ACCESS_KEY_ID && AWS_SECRET_ACCESS_KEY)
})

module.exports = {
s3Bucket,
s3Prefix,
s3Client
}
2 changes: 0 additions & 2 deletions src/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const HEADER = 'header'
const WEBSITE_HOST = 'dvc.org'

const FORUM_URL = `https://discuss.${WEBSITE_HOST}`
const BLOG_URL = `https://blog.${WEBSITE_HOST}`

const PAGE_DOC = '/doc'

Expand All @@ -24,7 +23,6 @@ const BLOG = {
module.exports = {
HEADER,
FORUM_URL,
BLOG_URL,
BLOG,
PAGE_DOC
}
11 changes: 11 additions & 0 deletions src/server/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict'

const { AWS_REGION, S3_BUCKET, HEROKU_APP_NAME } = process.env

const s3Prefix = HEROKU_APP_NAME
? `dvc-org-pulls/${HEROKU_APP_NAME}`
: 'dvc-org-prod'
const s3Bucket = S3_BUCKET
const s3Url = `http://${s3Bucket}.s3-website.${AWS_REGION}.amazonaws.com/${s3Prefix}`

module.exports = { s3Prefix, s3Bucket, s3Url }
43 changes: 43 additions & 0 deletions src/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Production server. Proxies to S3 depending on HEROKU_APP_NAME (see
* scripts/deploy-with-s3.js)
*
* NOTE: This file doesn't go through babel or webpack. Make sure the syntax and
* sources this file requires are compatible with the current node version you
* are running.
*
* Required environment variables:
*
* - S3_BUCKET: name of the bucket
* - AWS_REGION: region of the bucket
* - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY: IAM token to access bucket
* - HEROKU_APP_NAME: If this is a PR, an ID of the PR. Don't add this for
* production.
*/

const express = require('express')
const compression = require('compression')
const { s3Url } = require('./config')
const { isProduction } = require('./utils')

const port = process.env.PORT || 3000
const app = express()

const apiMiddleware = require('./middleware/api')
const redirectsMiddleware = require('./middleware/redirects')
const serveMiddleware = require('./middleware/serve')

app.use(compression())
app.use(redirectsMiddleware)
app.use('/api', apiMiddleware)
app.use(serveMiddleware)

app.listen(port, () => {
console.log(`Listening on http://0.0.0.0:${port}/`)

if (isProduction) {
console.log(`Proxying to ${s3Url}`)
} else {
console.log('Serving static files from local')
}
})
Loading

0 comments on commit 847d4ad

Please sign in to comment.