Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add d2-app-scripts deploy command #451

Merged
merged 8 commits into from
Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,20 @@
"@dhis2/app-shell": "5.0.0",
"@dhis2/cli-helpers-engine": "^1.5.0",
"archiver": "^3.1.1",
"axios": "^0.20.0",
"babel-jest": "^24.9.0",
"babel-plugin-react-require": "^3.1.3",
"chokidar": "^3.3.0",
"detect-port": "^1.3.0",
"dotenv": "^8.1.0",
"dotenv-expand": "^5.1.0",
"form-data": "^3.0.0",
"fs-extra": "^8.1.0",
"gaze": "^1.1.3",
"handlebars": "^4.3.3",
"i18next-conv": "^9",
"i18next-scanner": "^2.10.3",
"inquirer": "^7.3.3",
"jest-cli": "^24.9.0",
"lodash": "^4.17.11",
"parse-author": "^2.0.0",
Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const setAppParameters = (standalone, config) => {
}

const handler = async ({
cwd,
cwd = process.cwd(),
mode,
dev,
watch,
Expand Down Expand Up @@ -123,7 +123,7 @@ const handler = async ({
.replace(/{{version}}/, config.version)
reporter.info(
`Creating app archive at ${chalk.bold(
path.relative(process.cwd(), appBundle)
path.relative(cwd, appBundle)
)}...`
)
await bundleApp(paths.buildAppOutput, appBundle)
Expand Down
179 changes: 179 additions & 0 deletions cli/src/commands/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
const { reporter, chalk } = require('@dhis2/cli-helpers-engine')
const path = require('path')
const fs = require('fs-extra')
const FormData = require('form-data')
const inquirer = require('inquirer')

const makePaths = require('../lib/paths')
const parseConfig = require('../lib/parseConfig')
const { createClient } = require('../lib/httpClient')
const { constructAppUrl } = require('../lib/constructAppUrl')

const dumpHttpError = (message, response) => {
if (!response) {
reporter.error(message)
return
}

reporter.error(
message,
response.status,
typeof response.data === 'object'
? response.data.message
: response.statusText
)
reporter.debugErr('Error details', response.data)
}

const promptForDhis2Config = async params => {
if (
process.env.CI &&
(!params.baseUrl || !params.username || !params.password)
) {
reporter.error(
'Prompt disabled in CI mode - missing baseUrl, username, or password parameter.'
)
process.exit(1)
}

const isValidUrl = input =>
input && input.length && input.match(/^https?:\/\/[^/.]+/)

const responses = await inquirer.prompt([
{
type: 'input',
name: 'baseUrl',
message: 'DHIS2 instance URL:',
when: () => !params.baseUrl,
validate: input =>
isValidUrl(input)
? true
: 'Please enter a valid URL, it must start with http:// or https://',
},
{
type: 'input',
name: 'username',
message: 'DHIS2 instance username:',
when: () => !params.username,
},
{
type: 'password',
name: 'password',
message: 'DHIS2 instance password:',
when: () => !params.password,
},
])

return {
baseUrl: responses.baseUrl || params.baseUrl,
auth: {
username: responses.username || params.username,
password: responses.password || params.password,
},
}
}

const handler = async ({ cwd = process.cwd(), ...params }) => {
const paths = makePaths(cwd)
const config = parseConfig(paths)

const dhis2Config = await promptForDhis2Config(params)

if (config.standalone) {
reporter.error(`Standalone apps cannot be deployed to DHIS2 instances`)
process.exit(1)
}

const appBundle = path.relative(
cwd,
paths.buildAppBundle
.replace(/{{name}}/, config.name)
.replace(/{{version}}/, config.version)
)

if (!fs.existsSync(appBundle)) {
reporter.error(
`App bundle does not exist, run ${chalk.bold(
'd2-app-scripts build'
)} before deploying.`
)
process.exit(1)
}

const client = createClient(dhis2Config)
const formData = new FormData()
formData.append('file', fs.createReadStream(appBundle))

let serverVersion
try {
reporter.print(`Pinging server ${dhis2Config.baseUrl}...`)
const rawServerVersion = (await client.get('/api/system/info.json'))
.data.version
const parsedServerVersion = /(\d+)\.(\d+)/.exec(rawServerVersion)
if (!parsedServerVersion) {
reporter.error(
`Invalid server version ${rawServerVersion} found, aborting...`
)
process.exit(1)
}
serverVersion = {
full: parsedServerVersion[0],
major: parsedServerVersion[1],
minor: parsedServerVersion[2],
}
reporter.debug(
'Found server version',
serverVersion.full,
`(${rawServerVersion})`
)
} catch (e) {
dumpHttpError(
`Server ${chalk.bold(dhis2Config.baseUrl)} could not be contacted`,
e.response
)
process.exit(1)
}

const appUrl = constructAppUrl(dhis2Config.baseUrl, config, serverVersion)

try {
reporter.print('Uploading app bundle...')
await client.post('/api/apps', formData, {
headers: {
...formData.getHeaders(),
},
timeout: 30000, // Ensure we have enough time to upload a large zip file
})
reporter.info(
`Successfully deployed ${config.name} to ${dhis2Config.baseUrl}`
)
} catch (e) {
dumpHttpError('Failed to upload app, HTTP error', e.response)
process.exit(1)
}

reporter.debug(`Testing app launch url at ${appUrl}...`)
try {
await client.get(appUrl)
} catch (e) {
dumpHttpError(`Uploaded app not responding at ${appUrl}`)
process.exit(1)
}
reporter.print(`App is available at ${appUrl}`)
}

const command = {
command: 'deploy [baseUrl]',
alias: 'd',
desc: 'Deploy the built application to a specific DHIS2 instance',
builder: {
username: {
alias: 'u',
description:
'The username for authenticating with the DHIS2 instance',
},
},
handler,
}

module.exports = command
28 changes: 28 additions & 0 deletions cli/src/lib/constructAppUrl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module.exports.constructAppUrl = (baseUrl, config, serverVersion) => {
let appUrl = baseUrl

const isModernServer = serverVersion.major >= 2 && serverVersion.minor >= 35

// From core version 2.35, short_name is used instead of the human-readable title to generate the url slug
const urlSafeAppSlug = (isModernServer ? config.name : config.title)
.replace(/[^A-Za-z0-9\s-]/g, '')
.replace(/\s+/g, '-')

// From core version 2.35, core apps are hosted at the server root under the /dhis-web-* namespace, whilst
if (config.coreApp && isModernServer) {
appUrl += `/dhis-web-${urlSafeAppSlug}/`
} else {
appUrl += `/api/apps/${urlSafeAppSlug}/`
}

// Prior to core version 2.35, installed applications did not properly serve "pretty" urls (`/` vs `/index.html`)
if (!isModernServer) {
appUrl += 'index.html'
}

// Clean up any double slashes
const scheme = appUrl.substr(0, appUrl.indexOf('://') + 3)
appUrl =
scheme + appUrl.substr(scheme.length).replace(/(?!https?:)\/+/g, '/')
return appUrl
}
9 changes: 9 additions & 0 deletions cli/src/lib/httpClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const axios = require('axios').default

module.exports.createClient = ({ baseUrl, auth, ...options }) => {
return axios.create({
baseURL: baseUrl,
auth: auth,
...options,
})
}
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [`d2-app-scripts build`](scripts/build.md)
- [`d2-app-scripts start`](scripts/start.md)
- [`d2-app-scripts test`](scripts/test.md)
- [`d2-app-scripts deploy`](scripts/test.md)
- [**Configuration**](config.md)
- [Types - `app`, `lib`](config/types)
- [`d2.config.js` Reference](config/d2-config-js-reference.md)
Expand Down
29 changes: 29 additions & 0 deletions docs/scripts/deploy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# d2 app scripts deploy

Deploys a built application bundle to a running DHIS2 instance.

Note that you must run `d2 app scripts build` **before** running `deploy`

This command will prompt the user for the URL of the DHIS2 instance as well as a username and password to authenticate with that instance. The URL can be passed optionally as the first positional argument to the command, and the username with the `--username` or `-u` option. The password can be specified with the `D2_PASSWORD` environment variable. For example, the following will deploy the app without waiting for user input.

```sh
> d2 app scripts build
> export D2_PASSWORD=district
> d2 app scripts deploy https://play.dhis2.org/dev --username admin
```

## Usage

```sh
> d2 app scripts deploy --help
d2-app-scripts deploy [baseUrl]

Deploy the built application to a specific DHIS2 instance

Options:
--cwd working directory to use (defaults to cwd)
--version Show version number [boolean]
--config Path to JSON config file
--username, -u The username for authenticating with the DHIS2 instance
-h, --help Show help [boolean]
```
3 changes: 2 additions & 1 deletion examples/simple-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"scripts": {
"start": "../../cli/bin/d2-app-scripts start",
"build": "../../cli/bin/d2-app-scripts build",
"test": "../../cli/bin/d2-app-scripts test"
"test": "../../cli/bin/d2-app-scripts test",
"deploy": "../../cli/bin/d2-app-scripts deploy"
},
"resolutions": {
"@dhis2/app-shell": "file:../../shell",
Expand Down
Loading