Skip to content

Commit

Permalink
feat(command-env): add migrate subcommand (#3876)
Browse files Browse the repository at this point in the history
* feat: add env transfer subcommand

* feat: add test for env transfer

* chore: update docs

* chore: fix format

* chore: fix test

* refactor: user options instead of arguments, add error handling

* fix: update docs

* test: fix tests in CI

* feat: rename to migrate

Co-authored-by: erezrokah <erezrokah@users.noreply.github.com>
  • Loading branch information
ashalfarhan and erezrokah committed Dec 29, 2021
1 parent a8ae1c5 commit 290f9a8
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ Local dev server
| [`env:get`](/docs/commands/env.md#envget) | Get resolved value of specified environment variable (includes netlify.toml) |
| [`env:import`](/docs/commands/env.md#envimport) | Import and set environment variables from .env file |
| [`env:list`](/docs/commands/env.md#envlist) | Lists resolved environment variables for site (includes netlify.toml) |
| [`env:migrate`](/docs/commands/env.md#envmigrate) | Migrate environment variables from one site to another |
| [`env:set`](/docs/commands/env.md#envset) | Set value of environment variable |
| [`env:unset`](/docs/commands/env.md#envunset) | Unset an environment variable which removes it from the UI |

Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Local dev server
| [`env:get`](/docs/commands/env.md#envget) | Get resolved value of specified environment variable (includes netlify.toml) |
| [`env:import`](/docs/commands/env.md#envimport) | Import and set environment variables from .env file |
| [`env:list`](/docs/commands/env.md#envlist) | Lists resolved environment variables for site (includes netlify.toml) |
| [`env:migrate`](/docs/commands/env.md#envmigrate) | Migrate environment variables from one site to another |
| [`env:set`](/docs/commands/env.md#envset) | Set value of environment variable |
| [`env:unset`](/docs/commands/env.md#envunset) | Unset an environment variable which removes it from the UI |

Expand Down
28 changes: 28 additions & 0 deletions docs/commands/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ netlify env
| [`env:get`](/docs/commands/env.md#envget) | Get resolved value of specified environment variable (includes netlify.toml) |
| [`env:import`](/docs/commands/env.md#envimport) | Import and set environment variables from .env file |
| [`env:list`](/docs/commands/env.md#envlist) | Lists resolved environment variables for site (includes netlify.toml) |
| [`env:migrate`](/docs/commands/env.md#envmigrate) | Migrate environment variables from one site to another |
| [`env:set`](/docs/commands/env.md#envset) | Set value of environment variable |
| [`env:unset`](/docs/commands/env.md#envunset) | Unset an environment variable which removes it from the UI |

Expand All @@ -37,6 +38,7 @@ netlify env:get VAR_NAME
netlify env:set VAR_NAME value
netlify env:unset VAR_NAME
netlify env:import fileName
netlify env:migrate --to <to-site-id>
```

---
Expand Down Expand Up @@ -99,6 +101,32 @@ netlify env:list
- `httpProxy` (*string*) - Proxy server address to route requests through.
- `httpProxyCertificateFilename` (*string*) - Certificate file to use when connecting using a proxy server

---
## `env:migrate`

Migrate environment variables from one site to another

**Usage**

```bash
netlify env:migrate
```

**Flags**

- `from` (*string*) - Site ID (From)
- `to` (*string*) - Site ID (To)
- `debug` (*boolean*) - Print debugging information
- `httpProxy` (*string*) - Proxy server address to route requests through.
- `httpProxyCertificateFilename` (*string*) - Certificate file to use when connecting using a proxy server

**Examples**

```bash
netlify env:migrate --to <to-site-id>
netlify env:migrate --to <to-site-id> --from <from-site-id>
```

---
## `env:set`

Expand Down
1 change: 1 addition & 0 deletions docs/commands/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Local dev server
| [`env:get`](/docs/commands/env.md#envget) | Get resolved value of specified environment variable (includes netlify.toml) |
| [`env:import`](/docs/commands/env.md#envimport) | Import and set environment variables from .env file |
| [`env:list`](/docs/commands/env.md#envlist) | Lists resolved environment variables for site (includes netlify.toml) |
| [`env:migrate`](/docs/commands/env.md#envmigrate) | Migrate environment variables from one site to another |
| [`env:set`](/docs/commands/env.md#envset) | Set value of environment variable |
| [`env:unset`](/docs/commands/env.md#envunset) | Unset an environment variable which removes it from the UI |

Expand Down
108 changes: 108 additions & 0 deletions src/commands/env/env-migrate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// @ts-check

const { isEmpty } = require('lodash')

const { chalk, error: logError, log } = require('../../utils')

const safeGetSite = async (api, siteId) => {
try {
const data = await api.getSite({ siteId })
return { data }
} catch (error) {
return { error }
}
}

/**
* The env:migrate command
* @param {string} siteIdA Site (From)
* @param {string} siteIdB Site (To)
* @param {import('commander').OptionValues} options
* @param {import('../base-command').BaseCommand} command
* @returns {Promise<boolean>}
*/
const envMigrate = async (options, command) => {
const { api, site } = command.netlify

if (!site.id && !options.from) {
log(
'Please include the source site Id as the `--from` option, or run `netlify link` to link this folder to a Netlify site',
)
return false
}

const siteId = {
from: options.from || site.id,
to: options.to,
}

const [{ data: siteFrom, error: errorFrom }, { data: siteTo, error: errorTo }] = await Promise.all([
safeGetSite(api, siteId.from),
safeGetSite(api, siteId.to),
])

if (errorFrom) {
logError(`Can't find site with id ${chalk.bold(siteId.from)}. Please make sure the site exists.`)
return false
}

if (errorTo) {
logError(`Can't find site with id ${chalk.bold(siteId.to)}. Please make sure the site exists.`)
return false
}

const [
{
build_settings: { env: envFrom = {} },
},
{
build_settings: { env: envTo = {} },
},
] = [siteFrom, siteTo]

if (isEmpty(envFrom)) {
log(`${chalk.greenBright(siteFrom.name)} has no environment variables, nothing to migrate`)
return false
}

// Merge from site A to site B
const mergedEnv = {
...envTo,
...envFrom,
}

// Apply environment variable updates
await api.updateSite({
siteId: siteId.to,
body: {
build_settings: {
env: mergedEnv,
},
},
})

log(
`Successfully migrated environment variables from ${chalk.greenBright(siteFrom.name)} to ${chalk.greenBright(
siteTo.name,
)}`,
)
}

/**
* Creates the `netlify env:migrate` command
* @param {import('../base-command').BaseCommand} program
* @returns
*/
const createEnvMigrateCommand = (program) =>
program
.command('env:migrate')
.option('-f, --from <from>', 'Site ID (From)')
.requiredOption('-t, --to <to>', 'Site ID (To)')
.description(`Migrate environment variables from one site to another`)
.addExamples([
'netlify env:migrate --to <to-site-id>',
'netlify env:migrate --to <to-site-id> --from <from-site-id>',
])
.action(envMigrate)

module.exports = { createEnvMigrateCommand }
3 changes: 3 additions & 0 deletions src/commands/env/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const { createEnvGetCommand } = require('./env-get')
const { createEnvImportCommand } = require('./env-import')
const { createEnvListCommand } = require('./env-list')
const { createEnvMigrateCommand } = require('./env-migrate')
const { createEnvSetCommand } = require('./env-set')
const { createEnvUnsetCommand } = require('./env-unset')

Expand All @@ -25,6 +26,7 @@ const createEnvCommand = (program) => {
createEnvListCommand(program)
createEnvSetCommand(program)
createEnvUnsetCommand(program)
createEnvMigrateCommand(program)

return program
.command('env')
Expand All @@ -35,6 +37,7 @@ const createEnvCommand = (program) => {
'netlify env:set VAR_NAME value',
'netlify env:unset VAR_NAME',
'netlify env:import fileName',
'netlify env:migrate --to <to-site-id>',
])
.action(env)
}
Expand Down
113 changes: 113 additions & 0 deletions tests/command.env.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -460,3 +460,116 @@ test('env:import --json --replace-existing should replace all existing vars and
})
})
})

test("env:migrate should return without migrate if there's no env in source site", async (t) => {
await withSiteBuilder('site-env', async (builder) => {
await builder.buildAsync()
const createRoutes = [
{ path: 'sites/site_id', response: { ...siteInfo, build_settings: { env: {} } } },
{ path: 'sites/site_id_a', response: { ...siteInfo, build_settings: { env: {} } } },
]
await withMockApi(createRoutes, async ({ apiUrl }) => {
const cliResponse = await callCli(['env:migrate', '--to', 'site_id_a'], getCLIOptions({ builder, apiUrl }))

t.snapshot(normalize(cliResponse))
})
})
})

test("env:migrate should print error if --to site doesn't exist", async (t) => {
await withSiteBuilder('site-env', async (builder) => {
await builder.buildAsync()
const createRoutes = [{ path: 'sites/site_id', response: { ...siteInfo, build_settings: { env: {} } } }]
await withMockApi(createRoutes, async ({ apiUrl }) => {
const { stderr: cliResponse } = await t.throwsAsync(
callCli(['env:migrate', '--to', 'to-site'], getCLIOptions({ builder, apiUrl })),
)

t.true(cliResponse.includes(`Can't find site with id to-site. Please make sure the site exists`))
})
})
})

test("env:migrate should print error if --from site doesn't exist", async (t) => {
await withSiteBuilder('site-env', async (builder) => {
await builder.buildAsync()
await withMockApi([], async ({ apiUrl }) => {
const { stderr: cliResponse } = await t.throwsAsync(
callCli(['env:migrate', '--from', 'from-site', '--to', 'to-site'], getCLIOptions({ builder, apiUrl })),
)

t.true(cliResponse.includes(`Can't find site with id from-site. Please make sure the site exists`))
})
})
})

test('env:migrate should exit if the folder is not linked to a site, and --from is not provided', async (t) => {
await withSiteBuilder('site-env', async (builder) => {
await builder.buildAsync()

const cliResponse = await callCli(['env:migrate', '--to', 'site_id_a'], {
cwd: builder.directory,
extendEnv: false,
PATH: process.env.PATH,
})
t.snapshot(normalize(cliResponse))
})
})

test('env:migrate should return success message', async (t) => {
const envFrom = {
migrate_me: 'migrate_me',
}

const envTo = {
existing_env: 'existing_env',
}

const siteInfoTo = {
...siteInfo,
id: 'site_id_a',
name: 'site-name-a',
}

const newBuildSettings = {
env: {
...envFrom,
...envTo,
},
}
const expectedPatchRequest = {
path: 'sites/site_id_a',
method: 'PATCH',
requestBody: {
build_settings: newBuildSettings,
},
response: {
...siteInfoTo,
build_settings: newBuildSettings,
},
}
const migrateRoutes = [
{ path: 'sites/site_id', response: { ...siteInfo, build_settings: { env: envFrom } } },
{ path: 'sites/site_id_a', response: { ...siteInfoTo, build_settings: { env: envTo } } },
{ path: 'sites/site_id/service-instances', response: [] },
{
path: 'accounts',
response: [{ slug: siteInfo.account_slug }],
},
expectedPatchRequest,
]

await withSiteBuilder('site-env', async (builder) => {
await builder.buildAsync()
await withMockApi(migrateRoutes, async ({ apiUrl, requests }) => {
const cliResponse = await callCli(['env:migrate', '--to', 'site_id_a'], getCLIOptions({ apiUrl, builder }))

t.snapshot(normalize(cliResponse))

const patchRequest = requests.find(
(request) => request.method === 'PATCH' && request.path === '/api/v1/sites/site_id_a',
)
t.deepEqual(patchRequest.body, expectedPatchRequest.requestBody)
})
})
})
18 changes: 18 additions & 0 deletions tests/snapshots/command.env.test.js.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,21 @@ Generated by [AVA](https://avajs.dev).
| DB_ADMIN | admin |␊
| DB_PASSWORD | 1234 |␊
'---------------------'`

## env:migrate should return without migrate if there's no env in source site

> Snapshot 1
'site-name has no environment variables, nothing to migrate'

## env:migrate should exit if the folder is not linked to a site, and --from is not provided

> Snapshot 1
'Please include the source site Id as the `--from` option, or run `netlify link` to link this folder to a Netlify site'

## env:migrate should return success message

> Snapshot 1
'Successfully migrated environment variables from site-name to site-name-a'
Binary file modified tests/snapshots/command.env.test.js.snap
Binary file not shown.

1 comment on commit 290f9a8

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📊 Benchmark results

Package size: 359 MB

Please sign in to comment.