platformatic 0.20.1 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -12,6 +12,7 @@ import { isColorSupported } from 'colorette'
12
12
  import helpMe from 'help-me'
13
13
  import { upgrade } from './lib/upgrade.js'
14
14
  import { gh } from './lib/gh.js'
15
+ import { deploy } from './lib/deploy.js'
15
16
 
16
17
  import { logo } from './lib/ascii.js'
17
18
 
@@ -44,6 +45,7 @@ program.register('help db', async (args) => runDB(['help', ...args]))
44
45
  program.register('help service', async (args) => runService(['help', ...args]))
45
46
  program.register({ command: 'login', strict: true }, login)
46
47
  program.register('gh', gh)
48
+ program.register('deploy', deploy)
47
49
 
48
50
  const args = minimist(process.argv.slice(2), {
49
51
  boolean: ['help', 'version'],
@@ -0,0 +1,22 @@
1
+ Deploys a Platformatic application to the cloud
2
+
3
+ ``` bash
4
+ $ platformatic deploy
5
+ ```
6
+
7
+ Options:
8
+
9
+ -t, --type static/dynamic The type of the workspace.
10
+ -c, --config FILE Specify a configuration file to use
11
+ -l --label TEXT The deploy label. Only for dynamic workspaces.
12
+ -e --env FILE The environment file to use. Default: ".env"
13
+ -s --secrets FILE The secrets file to use. Default: ".secrets.env"
14
+ --workspace-id uuid The workspace id where the application will be deployed
15
+ --workspace-key TEXT The workspace key where the application will be deployed
16
+
17
+ To deploy a Platformatic application to the cloud you should go to the
18
+ Platformatic cloud dashboard and create a workspace. Then you can get your
19
+ workspace id and key from the workspace settings page.
20
+
21
+ Tp deploy an application to a dynamic workspace, you will need to specify the
22
+ deploy label. You can get it in you cloud dashboard or specify a new one.
package/help/help.txt CHANGED
@@ -6,3 +6,4 @@ Welcome to Platformatic. Available commands are:
6
6
  * service - start Platformatic Service; type `platformatic service help` to know more.
7
7
  * upgrade - upgrade the Platformatic configuration to the latest version.
8
8
  * gh - creates a new gh action for Platformatic deployments
9
+ * deploy - deploy a Platformatic application to the cloud
package/lib/deploy.js ADDED
@@ -0,0 +1,143 @@
1
+ 'use strict'
2
+
3
+ import { isAbsolute, dirname, relative } from 'path'
4
+
5
+ import pino from 'pino'
6
+ import pretty from 'pino-pretty'
7
+ import inquirer from 'inquirer'
8
+ import parseArgs from 'minimist'
9
+ import deployClient from '@platformatic/deploy-client'
10
+
11
+ export const DEPLOY_SERVICE_HOST = 'https://plt-production-deploy-service.fly.dev'
12
+
13
+ const WORKSPACE_TYPES = ['static', 'dynamic']
14
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
15
+
16
+ const logger = pino(pretty({
17
+ translateTime: 'SYS:HH:MM:ss',
18
+ ignore: 'hostname,pid'
19
+ }))
20
+
21
+ export async function deploy (argv) {
22
+ try {
23
+ const args = parseArgs(argv, {
24
+ alias: {
25
+ config: 'c',
26
+ type: 't',
27
+ label: 'l',
28
+ env: 'e',
29
+ secrets: 's'
30
+ },
31
+ string: [
32
+ 'type',
33
+ 'label',
34
+ 'workspace-id',
35
+ 'workspace-key',
36
+ 'env',
37
+ 'secrets',
38
+ 'deploy-service-host'
39
+ ],
40
+ default: {
41
+ 'deploy-service-host': DEPLOY_SERVICE_HOST
42
+ }
43
+ })
44
+
45
+ let workspaceType = args.type
46
+ /* c8 ignore next 9 */
47
+ if (!workspaceType) {
48
+ const answer = await inquirer.prompt({
49
+ type: 'list',
50
+ name: 'workspaceType',
51
+ message: 'Select workspace type:',
52
+ choices: WORKSPACE_TYPES
53
+ })
54
+ workspaceType = answer.workspaceType
55
+ }
56
+
57
+ if (!WORKSPACE_TYPES.includes(workspaceType)) {
58
+ throw new Error(
59
+ `Invalid workspace type provided: "${workspaceType}". ` +
60
+ `Type must be one of: ${WORKSPACE_TYPES.join(', ')}.`
61
+ )
62
+ }
63
+
64
+ let workspaceId = args['workspace-id']
65
+ /* c8 ignore next 8 */
66
+ if (!workspaceId) {
67
+ const answer = await inquirer.prompt({
68
+ type: 'input',
69
+ name: 'workspaceId',
70
+ message: 'Enter workspace id:'
71
+ })
72
+ workspaceId = answer.workspaceId
73
+ }
74
+
75
+ if (!UUID_REGEX.test(workspaceId)) {
76
+ throw new Error('Invalid workspace id provided. Workspace id must be a valid uuid.')
77
+ }
78
+
79
+ let workspaceKey = args['workspace-key']
80
+ /* c8 ignore next 9 */
81
+ if (!workspaceKey) {
82
+ const answer = await inquirer.prompt({
83
+ type: 'password',
84
+ name: 'workspaceKey',
85
+ message: 'Enter workspace key:',
86
+ mask: '*'
87
+ })
88
+ workspaceKey = answer.workspaceKey
89
+ }
90
+
91
+ let label = args.label
92
+ if (workspaceType === 'dynamic') {
93
+ /* c8 ignore next 9 */
94
+ if (!label) {
95
+ const answer = await inquirer.prompt({
96
+ type: 'input',
97
+ name: 'label',
98
+ message: 'Enter deploy label:',
99
+ default: 'cli:deploy-1'
100
+ })
101
+ label = answer.label
102
+ }
103
+
104
+ const labelPrefix = label.split(':')[0]
105
+ const labelPrefixes = ['cli', 'github-pr']
106
+ if (!labelPrefix || !labelPrefixes.includes(labelPrefix)) {
107
+ throw new Error(
108
+ `Invalid deploy label provided: "${label}". ` +
109
+ `Label must be prefixed with one of: ${labelPrefixes.join(', ')}.`
110
+ )
111
+ }
112
+ }
113
+
114
+ let pathToConfig = args.config
115
+ let pathToProject = process.cwd()
116
+
117
+ if (pathToConfig && isAbsolute(pathToConfig)) {
118
+ pathToProject = dirname(pathToConfig)
119
+ pathToConfig = relative(pathToProject, pathToConfig)
120
+ }
121
+
122
+ const pathToEnvFile = args.env || '.env'
123
+ const pathToSecretsFile = args.secrets || '.secrets.env'
124
+ const deployServiceHost = args['deploy-service-host']
125
+
126
+ await deployClient.deploy({
127
+ deployServiceHost,
128
+ workspaceId,
129
+ workspaceKey,
130
+ pathToProject,
131
+ pathToConfig,
132
+ pathToEnvFile,
133
+ pathToSecretsFile,
134
+ secrets: {},
135
+ variables: {},
136
+ label,
137
+ logger
138
+ })
139
+ } catch (err) {
140
+ logger.error(err.message)
141
+ process.exit(1)
142
+ }
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "platformatic",
3
- "version": "0.20.1",
3
+ "version": "0.21.1",
4
4
  "description": "Platformatic CLI",
5
5
  "main": "cli.js",
6
6
  "type": "module",
@@ -35,9 +35,11 @@
35
35
  "split2": "^4.1.0",
36
36
  "standard": "^17.0.0",
37
37
  "tap": "^16.3.4",
38
- "undici": "^5.20.0"
38
+ "undici": "^5.20.0",
39
+ "fastify": "^4.13.0"
39
40
  },
40
41
  "dependencies": {
42
+ "@platformatic/deploy-client": "^0.1.2",
41
43
  "colorette": "^2.0.19",
42
44
  "commist": "^3.2.0",
43
45
  "desm": "^1.3.0",
@@ -45,12 +47,13 @@
45
47
  "minimist": "^1.2.8",
46
48
  "pino": "^8.11.0",
47
49
  "pino-pretty": "^10.0.0",
48
- "@platformatic/authenticate": "0.20.1",
49
- "@platformatic/db": "0.20.1",
50
- "@platformatic/metaconfig": "0.20.1",
51
- "@platformatic/service": "0.20.1",
52
- "@platformatic/client-cli": "0.20.1",
53
- "create-platformatic": "0.20.1"
50
+ "inquirer": "^9.1.4",
51
+ "@platformatic/authenticate": "0.21.1",
52
+ "@platformatic/db": "0.21.1",
53
+ "@platformatic/metaconfig": "0.21.1",
54
+ "@platformatic/service": "0.21.1",
55
+ "@platformatic/client-cli": "0.21.1",
56
+ "create-platformatic": "0.21.1"
54
57
  },
55
58
  "scripts": {
56
59
  "test": "standard | snazzy && c8 --100 tap --no-coverage test/*.test.js",
@@ -0,0 +1,174 @@
1
+ import { test } from 'tap'
2
+ import { join } from 'desm'
3
+ import { execa } from 'execa'
4
+
5
+ import { cliPath, startDeployService, startMachine } from './helper.js'
6
+
7
+ test('should deploy to a static workspace to the cloud', async (t) => {
8
+ const workspaceType = 'static'
9
+ const workspaceId = 'b3d7f7e0-8c03-11e8-9eb6-529269fb1459'
10
+ const workspaceKey = 'b3d7f7e08c0311e89eb6529269fb1459'
11
+ const pathToConfig = join(import.meta.url, './fixtures/app-to-deploy/platformatic.db.json')
12
+
13
+ const machineHost = await startMachine(t)
14
+ const deployServiceHost = await startDeployService(t, {
15
+ createBundleCallback: (request, reply) => {
16
+ t.equal(request.headers['x-platformatic-workspace-id'], workspaceId)
17
+ t.equal(request.headers['x-platformatic-api-key'], workspaceKey)
18
+ t.match(request.body, {
19
+ bundle: {
20
+ appType: 'db',
21
+ configPath: 'platformatic.db.json'
22
+ }
23
+ })
24
+ t.ok(request.body.bundle.checksum)
25
+ },
26
+ createDeploymentCallback: (request, reply) => {
27
+ t.equal(request.headers['x-platformatic-workspace-id'], workspaceId)
28
+ t.equal(request.headers['x-platformatic-api-key'], workspaceKey)
29
+ t.same(
30
+ request.body,
31
+ {
32
+ variables: {
33
+ PLT_ENV_VARIABLE1: 'platformatic_variable1',
34
+ PLT_ENV_VARIABLE2: 'platformatic_variable2'
35
+ },
36
+ secrets: {
37
+ PLT_SECRET_1: 'platformatic_secret_1',
38
+ PLT_SECRET_2: 'platformatic_secret_2'
39
+ }
40
+ }
41
+ )
42
+ reply.code(200).send({ entryPointUrl: machineHost })
43
+ }
44
+ })
45
+
46
+ await execa('node', [
47
+ cliPath, 'deploy',
48
+ '--type', workspaceType,
49
+ '--config', pathToConfig,
50
+ '--workspace-id', workspaceId,
51
+ '--workspace-key', workspaceKey,
52
+ '--deploy-service-host', deployServiceHost
53
+ ])
54
+ })
55
+
56
+ test('should deploy to a dynamic workspace to the cloud', async (t) => {
57
+ const workspaceType = 'dynamic'
58
+ const workspaceId = 'b3d7f7e0-8c03-11e8-9eb6-529269fb1459'
59
+ const workspaceKey = 'b3d7f7e08c0311e89eb6529269fb1459'
60
+ const pathToConfig = join(import.meta.url, './fixtures/app-to-deploy/platformatic.db.json')
61
+ const label = 'cli:deploy-2'
62
+
63
+ const machineHost = await startMachine(t)
64
+ const deployServiceHost = await startDeployService(t, {
65
+ createBundleCallback: (request, reply) => {
66
+ t.equal(request.headers['x-platformatic-workspace-id'], workspaceId)
67
+ t.equal(request.headers['x-platformatic-api-key'], workspaceKey)
68
+ t.match(request.body, {
69
+ bundle: {
70
+ appType: 'db',
71
+ configPath: 'platformatic.db.json'
72
+ }
73
+ })
74
+ t.ok(request.body.bundle.checksum)
75
+ },
76
+ createDeploymentCallback: (request, reply) => {
77
+ t.equal(request.headers['x-platformatic-workspace-id'], workspaceId)
78
+ t.equal(request.headers['x-platformatic-api-key'], workspaceKey)
79
+ t.same(
80
+ request.body,
81
+ {
82
+ label,
83
+ variables: {
84
+ PLT_ENV_VARIABLE1: 'platformatic_variable1',
85
+ PLT_ENV_VARIABLE2: 'platformatic_variable2'
86
+ },
87
+ secrets: {
88
+ PLT_SECRET_1: 'platformatic_secret_1',
89
+ PLT_SECRET_2: 'platformatic_secret_2'
90
+ }
91
+ }
92
+ )
93
+ reply.code(200).send({ entryPointUrl: machineHost })
94
+ }
95
+ })
96
+
97
+ await execa('node', [
98
+ cliPath, 'deploy',
99
+ '--type', workspaceType,
100
+ '--label', label,
101
+ '--config', pathToConfig,
102
+ '--workspace-id', workspaceId,
103
+ '--workspace-key', workspaceKey,
104
+ '--deploy-service-host', deployServiceHost
105
+ ])
106
+ })
107
+
108
+ test('should fail if workspace id is not a uuid', async (t) => {
109
+ const workspaceType = 'static'
110
+ const workspaceId = 'not-a-uuid'
111
+ const workspaceKey = 'b3d7f7e08c0311e89eb6529269fb1459'
112
+ const pathToConfig = join(import.meta.url, './fixtures/app-to-deploy/platformatic.db.json')
113
+
114
+ try {
115
+ await execa('node', [
116
+ cliPath, 'deploy',
117
+ '--type', workspaceType,
118
+ '--config', pathToConfig,
119
+ '--workspace-id', workspaceId,
120
+ '--workspace-key', workspaceKey,
121
+ '--deploy-service-host', 'http://localhost:5555'
122
+ ])
123
+ t.fail('should have failed')
124
+ } catch (err) {
125
+ t.ok(err.message.includes('Invalid workspace id provided. Workspace id must be a valid uuid.'))
126
+ }
127
+ })
128
+
129
+ test('should fail if invalid workspace type provided', async (t) => {
130
+ const workspaceType = 'wrong'
131
+ const workspaceId = 'b3d7f7e0-8c03-11e8-9eb6-529269fb1459'
132
+ const workspaceKey = 'b3d7f7e08c0311e89eb6529269fb1459'
133
+ const pathToConfig = join(import.meta.url, './fixtures/app-to-deploy/platformatic.db.json')
134
+
135
+ try {
136
+ await execa('node', [
137
+ cliPath, 'deploy',
138
+ '--type', workspaceType,
139
+ '--config', pathToConfig,
140
+ '--workspace-id', workspaceId,
141
+ '--workspace-key', workspaceKey,
142
+ '--deploy-service-host', 'http://localhost:5555'
143
+ ])
144
+ t.fail('should have failed')
145
+ } catch (err) {
146
+ t.ok(err.message.includes('Invalid workspace type provided'))
147
+ }
148
+ })
149
+
150
+ test('should fail if deploy label does not start with a "cli:" prefix', async (t) => {
151
+ const workspaceType = 'dynamic'
152
+ const workspaceId = 'b3d7f7e0-8c03-11e8-9eb6-529269fb1459'
153
+ const workspaceKey = 'b3d7f7e08c0311e89eb6529269fb1459'
154
+ const pathToConfig = join(import.meta.url, './fixtures/app-to-deploy/platformatic.db.json')
155
+ const label = 'my label'
156
+
157
+ try {
158
+ await execa('node', [
159
+ cliPath, 'deploy',
160
+ '--type', workspaceType,
161
+ '--label', label,
162
+ '--config', pathToConfig,
163
+ '--workspace-id', workspaceId,
164
+ '--workspace-key', workspaceKey,
165
+ '--deploy-service-host', 'http://localhost:5555'
166
+ ])
167
+ t.fail('should have failed')
168
+ } catch (err) {
169
+ t.ok(err.message.includes(
170
+ `Invalid deploy label provided: "${label}". ` +
171
+ 'Label must be prefixed with one of: cli, github-pr.'
172
+ ))
173
+ }
174
+ })
@@ -0,0 +1,2 @@
1
+ PLT_ENV_VARIABLE1=platformatic_variable1
2
+ PLT_ENV_VARIABLE2=platformatic_variable2
@@ -0,0 +1,2 @@
1
+ PLT_SECRET_1=platformatic_secret_1
2
+ PLT_SECRET_2=platformatic_secret_2
@@ -0,0 +1,7 @@
1
+ {
2
+ "devDependencies": {
3
+ "@platformatic/sql-graphql": "^0.9.2",
4
+ "@platformatic/sql-mapper": "^0.9.2",
5
+ "platformatic": "^0.11.0"
6
+ }
7
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "server": {
3
+ "hostname": "127.0.0.1",
4
+ "port": 3042
5
+ },
6
+ "core": {
7
+ "connectionString": "sqlite://db.sqlite",
8
+ "graphql": true,
9
+ "ignore": {
10
+ "versions": true
11
+ }
12
+ },
13
+ "migrations": {
14
+ "dir": "migrations",
15
+ "table": "versions"
16
+ }
17
+ }
package/test/helper.js CHANGED
@@ -1,7 +1,61 @@
1
1
  import { join } from 'desm'
2
+ import fastify from 'fastify'
2
3
 
3
4
  const cliPath = join(import.meta.url, '..', 'cli.js')
4
5
 
6
+ async function startDeployService (t, options = {}) {
7
+ const deployService = fastify({ keepAliveTimeout: 1 })
8
+
9
+ deployService.post('/bundles', async (request, reply) => {
10
+ const createBundleCallback = options.createBundleCallback || (() => {})
11
+ await createBundleCallback(request, reply)
12
+
13
+ return {
14
+ id: 'default-bundle-id',
15
+ token: 'default-upload-token',
16
+ isBundleUploaded: false
17
+ }
18
+ })
19
+
20
+ deployService.post('/deployments', async (request, reply) => {
21
+ const createDeploymentCallback = options.createDeploymentCallback || (() => {})
22
+ await createDeploymentCallback(request, reply)
23
+ })
24
+
25
+ deployService.addContentTypeParser(
26
+ 'application/x-tar',
27
+ { bodyLimit: 1024 * 1024 * 1024 },
28
+ (request, payload, done) => done()
29
+ )
30
+
31
+ deployService.put('/upload', async (request, reply) => {
32
+ const uploadCallback = options.uploadCallback || (() => {})
33
+ await uploadCallback(request, reply)
34
+ })
35
+
36
+ t.teardown(async () => {
37
+ await deployService.close()
38
+ })
39
+
40
+ return deployService.listen({ port: 3042 })
41
+ }
42
+
43
+ async function startMachine (t, callback = () => {}) {
44
+ const machine = fastify({ keepAliveTimeout: 1 })
45
+
46
+ machine.get('/', async (request, reply) => {
47
+ await callback(request, reply)
48
+ })
49
+
50
+ t.teardown(async () => {
51
+ await machine.close()
52
+ })
53
+
54
+ return machine.listen({ port: 0 })
55
+ }
56
+
5
57
  export {
6
- cliPath
58
+ cliPath,
59
+ startDeployService,
60
+ startMachine
7
61
  }