Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions docs/commands/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ netlify deploy

- `alias` (*string*) - Specifies the alias for deployment, the string at the beginning of the deploy subdomain. Useful for creating predictable deployment URLs. Avoid setting an alias string to the same value as a deployed branch. `alias` doesn’t create a branch deploy and can’t be used in conjunction with the branch subdomain feature. Maximum 37 characters.
- `context` (*string*) - Specify a deploy context for environment variables read during the build ("production", "deploy-preview", "branch-deploy", "dev") or `branch:your-branch` where `your-branch` is the name of a branch (default: dev)
- `create-site` (*string*) - Create a new site and deploy to it. Optionally specify a name, otherwise a random name will be generated. Requires --team flag if you have multiple teams.
- `dir` (*string*) - Specify a folder to deploy
- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
- `functions` (*string*) - Specify a functions folder to deploy
Expand All @@ -43,8 +42,9 @@ netlify deploy
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in
- `prod` (*boolean*) - Deploy to production
- `site` (*string*) - A project name or ID to deploy to
- `site-name` (*string*) - Name for a new site. Implies --create-site if the site does not already exist.
- `skip-functions-cache` (*boolean*) - Ignore any functions created as part of a previous `build` or `deploy` commands, forcing them to be bundled again as part of the deployment
- `team` (*string*) - Specify team slug when creating a site. Only works with --create-site flag.
- `team` (*string*) - Specify team slug when creating a site. Only works with --create-site or --site-name flag.
- `timeout` (*string*) - Timeout to wait for deployment to finish
- `trigger` (*boolean*) - Trigger a new build of your project on Netlify without uploading local files

Expand All @@ -61,7 +61,7 @@ netlify deploy --message "A message with an $ENV_VAR"
netlify deploy --auth $NETLIFY_AUTH_TOKEN
netlify deploy --trigger
netlify deploy --context deploy-preview
netlify deploy --create-site my-new-site --team my-team # Create site and deploy
netlify deploy --site-name my-new-site --team my-team # Create site and deploy
```


Expand Down
63 changes: 63 additions & 0 deletions docs/commands/teams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
title: Netlify CLI teams command
sidebar:
label: teams
description: Manage Netlify teams via the command line
---

# `teams`

<!-- AUTO-GENERATED-CONTENT:START (GENERATE_COMMANDS_DOCS) -->
Handle various team operations
The teams command will help you manage your teams

**Usage**

```bash
netlify teams
```

**Flags**

- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

| Subcommand | description |

Check warning on line 25 in docs/commands/teams.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'Subcommand'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'Subcommand'?", "location": {"path": "docs/commands/teams.md", "range": {"start": {"line": 25, "column": 3}}}, "severity": "WARNING"}
|:--------------------------- |:-----|
| [`teams:list`](/commands/teams#teamslist) | List all teams you have access to |


**Examples**

```bash
netlify teams:list
```

---
## `teams:list`

List all teams you have access to

**Usage**

```bash
netlify teams:list
```

**Flags**

- `filter` (*string*) - For monorepos, specify the name of the application to run the command in
- `json` (*boolean*) - Output team data as JSON
- `debug` (*boolean*) - Print debugging information
- `auth` (*string*) - Netlify auth token - can be used to run this command without logging in

**Examples**

```bash
netlify teams:list
netlify teams:list --json
```

---

<!-- AUTO-GENERATED-CONTENT:END -->
9 changes: 9 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,15 @@

Switch your active Netlify account

### [teams](/commands/teams)

Handle various team operations
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you have any plans for what other operations would go here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah I think team level settings, team creation, etc. might go in here. Things agents might be able to do on a user's behalf


| Subcommand | description |

Check warning on line 189 in docs/index.md

View workflow job for this annotation

GitHub Actions / lint-docs

[vale] reported by reviewdog 🐶 [base.spelling] Spellcheck: did you really mean 'Subcommand'? Raw Output: {"message": "[base.spelling] Spellcheck: did you really mean 'Subcommand'?", "location": {"path": "docs/index.md", "range": {"start": {"line": 189, "column": 3}}}, "severity": "WARNING"}
|:--------------------------- |:-----|
| [`teams:list`](/commands/teams#teamslist) | List all teams you have access to |


### [unlink](/commands/unlink)

Unlink a local folder from a Netlify project
Expand Down
10 changes: 9 additions & 1 deletion src/commands/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,15 @@ const HELP_SEPARATOR_WIDTH = 5
* Those commands work with the system or are not writing any config files that need to be
* workspace aware.
*/
const COMMANDS_WITHOUT_WORKSPACE_OPTIONS = new Set(['api', 'recipes', 'completion', 'status', 'switch', 'login'])
const COMMANDS_WITHOUT_WORKSPACE_OPTIONS = new Set([
'api',
'recipes',
'completion',
'status',
'switch',
'login',
'teams',
])

/**
* A list of commands where we need to fetch featureflags for config resolution
Expand Down
76 changes: 57 additions & 19 deletions src/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { randomBytes } from 'crypto'
import { type Stats } from 'fs'
import { stat } from 'fs/promises'
import { basename, resolve } from 'path'
Expand Down Expand Up @@ -45,6 +46,7 @@ import { uploadSourceZip } from '../../utils/deploy/upload-source-zip.js'
import { getEnvelopeEnv } from '../../utils/env/index.js'
import { getFunctionsManifestPath, getInternalFunctionsDir } from '../../utils/functions/index.js'
import openBrowser from '../../utils/open-browser.js'
import { isInteractive } from '../../utils/scripted-commands.js'
import type BaseCommand from '../base-command.js'
import { link } from '../link/link.js'
import { sitesCreate } from '../sites/sites-create.js'
Expand Down Expand Up @@ -292,25 +294,21 @@ const generateDeployCommand = (
): string => {
const parts = ['netlify deploy']

// Handle site selection/creation first
if (options.createSite) {
const siteName = typeof options.createSite === 'string' ? options.createSite : '<SITE_NAME>'
parts.push(`--create-site ${siteName}`)
parts.push(`--site-name ${siteName}`)
if (availableTeams.length > 1) {
parts.push('--team <TEAM_SLUG>')
}
} else if (options.site) {
parts.push(`--site ${options.site}`)
} else {
parts.push('--create-site <SITE_NAME>')
if (availableTeams.length > 1) {
parts.push('--team <TEAM_SLUG>')
}
parts.push('--site <SITE>')
}

if (command?.options) {
for (const option of command.options) {
if (['createSite', 'site', 'team'].includes(option.attributeName())) {
if (['createSite', 'site', 'siteName', 'team'].includes(option.attributeName())) {
continue
}

Expand Down Expand Up @@ -352,9 +350,15 @@ const prepareProductionDeploy = async ({ api, siteData, options, command }) => {
if (isObject(siteData.published_deploy) && siteData.published_deploy.locked) {
log(`\n${NETLIFYDEVERR} Deployments are "locked" for production context of this project\n`)

// Generate copy-pasteable command with current options
const overrideCommand = generateDeployCommand({ ...options, prodIfUnlocked: true, prod: false }, [], command)

if (!isInteractive()) {
return logAndThrowError(
`Deployments are "locked" for production context of this project.\n\n` +
`To deploy anyway, use:\n ${overrideCommand}`,
)
}

log('\nTo override deployment lock (USE WITH CAUTION), use:')
log(` ${overrideCommand}`)
log('\nWarning: Only use --prod-if-unlocked if you are absolutely sure you want to override the deployment lock.\n')
Expand Down Expand Up @@ -960,27 +964,38 @@ const prepAndRunDeploy = async ({
return results
}

const resolveTeam = (
accounts: { slug: string; name: string; default?: boolean }[],
): (typeof accounts)[0] | undefined => {
if (accounts.length === 1) {
return accounts[0]
}
return accounts.find((acc) => acc.default)
}

const validateTeamForSiteCreation = (
accounts: { slug: string; name: string }[],
accounts: { slug: string; name: string; default?: boolean }[],
options: DeployOptionValues,
siteName?: string,
) => {
if (accounts.length === 0) {
return logAndThrowError('No teams available. Please ensure you have access to at least one team.')
}

if (accounts.length === 1) {
options.team = accounts[0].slug
const team = resolveTeam(accounts)
if (team) {
options.team = team.slug
const message = siteName ? `Creating new site: ${siteName}` : 'Creating new site with random name'
log(`${message} (using team: ${accounts[0].name})`)
log(`${message} (using team: ${team.name})`)
return
}

const availableTeams = accounts.map((team) => team.slug).join(', ')
const availableTeams = accounts.map((t) => t.slug).join(', ')
return logAndThrowError(
`Multiple teams available. Please specify which team to use with --team flag.\n` +
`Available teams: ${availableTeams}\n\n` +
`Example: netlify deploy --create-site${siteName ? ` ${siteName}` : ''} --team <TEAM_SLUG>`,
`Example: netlify deploy --site-name${siteName ? ` ${siteName}` : ' <SITE_NAME>'} --team <TEAM_SLUG>\n\n` +
`To list teams with full details, run: netlify teams:list`,
)
}

Expand Down Expand Up @@ -1014,12 +1029,27 @@ const createSiteWithFlags = async (options: DeployOptionValues, command: BaseCom
site.id = siteData.id
return siteData as SiteInfo
} catch (error_) {
if ((error_ as APIError).status === 422 && siteName) {
const suffix = randomBytes(4).toString('hex')
const suffixedName = `${siteName.trim()}-${suffix}`
log(`Site name "${siteName}" is taken. Retrying with "${suffixedName}"...`)
try {
const siteData = await api.createSiteInTeam({
accountSlug: options.team,
body: { name: suffixedName },
})
site.id = siteData.id
return siteData as SiteInfo
} catch (retryError) {
return logAndThrowError(
`Failed to create site "${suffixedName}": ${(retryError as APIError).status}: ${
(retryError as APIError).message
}`,
)
}
}
if ((error_ as APIError).status === 422) {
return logAndThrowError(
siteName
? `Site name "${siteName}" is already taken. Please try a different name.`
: 'Unable to create site with a random name. Please try again or specify a different name.',
)
return logAndThrowError('Unable to create site with a random name. Please try again or specify a different name.')
}
return logAndThrowError(`Failed to create site: ${(error_ as APIError).status}: ${(error_ as APIError).message}`)
}
Expand Down Expand Up @@ -1077,6 +1107,14 @@ const ensureSiteExists = async (
return createSiteWithFlags(options, command, site)
}

if (!isInteractive()) {
const { accounts } = command.netlify
options.createSite = true
validateTeamForSiteCreation(accounts, options)
log(`No project linked. Auto-creating a new project (team: ${options.team})...`)
return createSiteWithFlags(options, command, site)
}

return promptForSiteAction(options, command, site)
}

Expand Down
28 changes: 23 additions & 5 deletions src/commands/deploy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,17 @@ For detailed configuration options, see the Netlify documentation.`,
false,
)
.addOption(new Option('--upload-source-zip', 'Upload source code as a zip file').default(false).hideHelp(true))
.addOption(
new Option(
'--create-site [name]',
'Create a new site and deploy to it. Optionally specify a name, otherwise a random name will be generated. Uses your default team if --team is omitted.',
).hideHelp(true),
)
.option('--site-name <name>', 'Name for a new site. Implies --create-site if the site does not already exist.')
.option(
'--create-site [name]',
'Create a new site and deploy to it. Optionally specify a name, otherwise a random name will be generated. Requires --team flag if you have multiple teams.',
'--team <slug>',
'Specify team slug when creating a site. Only works with --create-site or --site-name flag.',
)
.option('--team <slug>', 'Specify team slug when creating a site. Only works with --create-site flag.')
.addExamples([
'netlify deploy',
'netlify deploy --site my-first-project',
Expand All @@ -98,7 +104,7 @@ For detailed configuration options, see the Netlify documentation.`,
'netlify deploy --auth $NETLIFY_AUTH_TOKEN',
'netlify deploy --trigger',
'netlify deploy --context deploy-preview',
'netlify deploy --create-site my-new-site --team my-team # Create site and deploy',
'netlify deploy --site-name my-new-site --team my-team # Create site and deploy',
])
.addHelpText('after', () => {
const docsUrl = 'https://docs.netlify.com/site-deploys/overview/'
Expand All @@ -123,8 +129,20 @@ For more information about Netlify deploys, see ${terminalLink(docsUrl, docsUrl,
return logAndThrowError('--context flag is only available when using the --build flag')
}

if (options.siteName) {
if (options.site) {
return logAndThrowError(
'Cannot specify both --site-name and --site. Use --site to deploy to an existing project.',
)
}
if (options.createSite && typeof options.createSite === 'string' && options.createSite !== options.siteName) {
return logAndThrowError('Cannot specify both --site-name and --create-site with different names.')
}
options.createSite = options.siteName
}

if (options.team && !options.createSite) {
return logAndThrowError('--team flag can only be used with --create-site flag')
return logAndThrowError('--team flag can only be used with --create-site or --site-name flag')
}

// Handle Windows + source zip upload
Expand Down
1 change: 1 addition & 0 deletions src/commands/deploy/option_values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type DeployOptionValues = BaseOptionValues & {
prod: boolean
prodIfUnlocked: boolean
site?: string
siteName?: string
skipFunctionsCache: boolean
team?: string
timeout?: number
Expand Down
2 changes: 2 additions & 0 deletions src/commands/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { createServeCommand } from './serve/index.js'
import { createSitesCommand } from './sites/index.js'
import { createStatusCommand } from './status/index.js'
import { createSwitchCommand } from './switch/index.js'
import { createTeamsCommand } from './teams/index.js'
import { AddressInUseError } from './types.js'
import { createUnlinkCommand } from './unlink/index.js'
import { createWatchCommand } from './watch/index.js'
Expand Down Expand Up @@ -232,6 +233,7 @@ export const createMainCommand = (): BaseCommand => {
createSitesCommand(program)
createStatusCommand(program)
createSwitchCommand(program)
createTeamsCommand(program)
createUnlinkCommand(program)
createWatchCommand(program)
createLogsCommand(program)
Expand Down
1 change: 1 addition & 0 deletions src/commands/teams/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createTeamsCommand } from './teams.js'
Loading
Loading