Skip to content
Draft
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
33 changes: 30 additions & 3 deletions src/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { execSync } from 'child_process'
import { randomBytes } from 'crypto'
import { type Stats } from 'fs'
import { stat } from 'fs/promises'
Expand Down Expand Up @@ -634,7 +635,7 @@ const runDeploy = async ({
config,
fnDir: functionDirectories,
functionsConfig,

branch: alias,
statusCb: silent ? () => {} : deployProgressCb(),
deployTimeout,
syncFileLimit: SYNC_FILE_LIMIT,
Expand Down Expand Up @@ -680,6 +681,7 @@ const runDeploy = async ({
}

const handleBuild = async ({
alias,
cachedConfig,
currentDir,
defaultConfig,
Expand All @@ -689,6 +691,7 @@ const handleBuild = async ({
packagePath,
skewProtectionToken,
}: {
alias?: string
cachedConfig: CachedConfig
currentDir: string
defaultConfig?: DefaultConfig | undefined
Expand All @@ -702,7 +705,11 @@ const handleBuild = async ({
return {}
}
const [token] = await getToken()
if (alias && !options.context) {
options.context = 'branch-deploy'
}
const resolvedOptions = await getRunBuildOptions({
branch: alias,
cachedConfig,
currentDir,
defaultConfig,
Expand Down Expand Up @@ -863,6 +870,7 @@ const printResults = ({
}

const prepAndRunDeploy = async ({
alias,
api,
command,
config,
Expand All @@ -874,14 +882,14 @@ const prepAndRunDeploy = async ({
workingDir,
deployId,
}: {
alias?: string
options: DeployOptionValues
command: BaseCommand
workingDir: string
deployId?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- FIXME(serhalp)
[key: string]: any
}) => {
const alias = options.alias || options.branch
// if a context is passed besides dev, we need to pull env vars from that specific context
if (options.context && options.context !== 'dev') {
command.netlify.cachedConfig.env = await getEnvelopeEnv({
Expand Down Expand Up @@ -1118,6 +1126,20 @@ const ensureSiteExists = async (
return promptForSiteAction(options, command, site)
}

const getLocalGitBranch = (): string | undefined => {
try {
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
}).trim()
if (branch && branch !== 'HEAD') {
return branch
}
} catch {
// not in a git repo
}
}

export const deploy = async (options: DeployOptionValues, command: BaseCommand) => {
const { workingDir } = command
const { api, site, siteInfo } = command.netlify
Expand All @@ -1136,6 +1158,8 @@ export const deploy = async (options: DeployOptionValues, command: BaseCommand)

const deployToProduction =
!options.draft && (options.prod || (options.prodIfUnlocked && !(siteData.published_deploy?.locked ?? false)))
const deployAlias =
!deployToProduction && !alias ? `cli-${getLocalGitBranch() ?? randomBytes(4).toString('hex')}` : alias

let results = {} as Awaited<ReturnType<typeof prepAndRunDeploy>>

Expand All @@ -1145,7 +1169,7 @@ export const deploy = async (options: DeployOptionValues, command: BaseCommand)
}

const draft = options.draft || (!deployToProduction && !alias)
const createDeployBody = { draft, branch: alias, include_upload_url: options.uploadSourceZip }
const createDeployBody = { draft, branch: deployAlias, include_upload_url: options.uploadSourceZip }

// TODO: Type this properly in `@netlify/api`.
const deployMetadata = (await api.createSiteDeploy({
Expand Down Expand Up @@ -1177,13 +1201,15 @@ export const deploy = async (options: DeployOptionValues, command: BaseCommand)
try {
const settings = await detectFrameworkSettings(command, 'build')
await handleBuild({
alias: deployAlias,
packagePath: command.workspacePackage,
cachedConfig: command.netlify.cachedConfig,
defaultConfig: getDefaultConfig(settings),
currentDir: command.workingDir,
options,
deployHandler: async ({ netlifyConfig }: { netlifyConfig: NetlifyConfig }) => {
results = await prepAndRunDeploy({
alias: deployAlias,
command,
options,
workingDir,
Expand Down Expand Up @@ -1214,6 +1240,7 @@ export const deploy = async (options: DeployOptionValues, command: BaseCommand)
}
} else {
results = await prepAndRunDeploy({
alias: deployAlias,
command,
options,
workingDir,
Expand Down
3 changes: 3 additions & 0 deletions src/lib/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ type EventHandler<T extends (opts: any) => void | Promise<void>> = {
// This is stored as `netlify.cachedConfig` and can be passed to
// `@netlify/build --cachedConfig`.
export const getRunBuildOptions = async ({
branch,
cachedConfig,
currentDir,
defaultConfig,
Expand All @@ -143,6 +144,7 @@ export const getRunBuildOptions = async ({
skewProtectionToken,
token,
}: {
branch?: string
cachedConfig: CachedConfig
currentDir: string
defaultConfig?: undefined | DefaultConfig
Expand Down Expand Up @@ -179,6 +181,7 @@ export const getRunBuildOptions = async ({
return {
cachedConfig,
defaultConfig: defaultConfig ?? {},
branch,
deployId,
siteId: cachedConfig.siteInfo.id,
accountId: cachedConfig.siteInfo.account_id,
Expand Down
78 changes: 78 additions & 0 deletions tests/integration/commands/deploy/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,84 @@ const withMockDeploy = async (fn: (mockApi: MockApi, deployState: DeployRouteSta
}
}

describe.concurrent('deploy branch handling', () => {
test('should set branch to cli-prefixed local git branch for non-prod deploys without alias', async (t) => {
await withMockDeploy(async (mockApi, deployState) => {
await withSiteBuilder(t, async (builder) => {
builder.withContentFile({
path: 'public/index.html',
content: '<h1>test</h1>',
})

await builder.build()

await callCli(
['deploy', '--json', '--no-build', '--dir', 'public'],
getCLIOptions({ apiUrl: mockApi.apiUrl, builder }),
).then(parseDeploy)

const createRequest = mockApi.requests.find((r) => r.method === 'POST' && r.path.includes('/deploys'))
const createBody = createRequest?.body as { branch?: string }
expect(createBody.branch).toBeDefined()
expect(createBody.branch).toMatch(/^cli-/)

const updateBody = deployState.getDeployBody()
expect(updateBody).not.toBeNull()
expect(updateBody!.branch).toBeDefined()
expect(updateBody!.branch).toMatch(/^cli-/)
})
})
})

test('should use explicit alias as branch without cli prefix', async (t) => {
await withMockDeploy(async (mockApi, deployState) => {
await withSiteBuilder(t, async (builder) => {
builder.withContentFile({
path: 'public/index.html',
content: '<h1>test</h1>',
})

await builder.build()

await callCli(
['deploy', '--json', '--no-build', '--dir', 'public', '--alias', 'my-custom-alias'],
getCLIOptions({ apiUrl: mockApi.apiUrl, builder }),
).then(parseDeploy)

const createRequest = mockApi.requests.find((r) => r.method === 'POST' && r.path.includes('/deploys'))
const createBody = createRequest?.body as { branch?: string }
expect(createBody.branch).toBe('my-custom-alias')

const updateBody = deployState.getDeployBody()
expect(updateBody).not.toBeNull()
expect(updateBody!.branch).toBe('my-custom-alias')
})
})
})

test('should not set branch for prod deploys without alias', async (t) => {
await withMockDeploy(async (mockApi) => {
await withSiteBuilder(t, async (builder) => {
builder.withContentFile({
path: 'public/index.html',
content: '<h1>test</h1>',
})

await builder.build()

await callCli(
['deploy', '--json', '--no-build', '--dir', 'public', '--prod'],
getCLIOptions({ apiUrl: mockApi.apiUrl, builder }),
).then(parseDeploy)

const createRequest = mockApi.requests.find((r) => r.method === 'POST' && r.path.includes('/deploys'))
const createBody = createRequest?.body as { branch?: string }
expect(createBody.branch).toBeUndefined()
})
})
})
})

describe.concurrent('deploy command', () => {
test('should deploy project when dir flag is passed', async (t) => {
await withMockDeploy(async (mockApi, deployState) => {
Expand Down
Loading