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
207 changes: 207 additions & 0 deletions __tests__/functions/prechecks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3674,3 +3674,210 @@ test('runs prechecks and finds the PR is NOT behind the stable branch (HAS_HOOKS
'beefdead'
)
})

// Tests for branch existence checks
class NotFoundError extends Error {
constructor(message) {
super(message)
this.status = 404
}
}

class UnexpectedError extends Error {
constructor(message) {
super(message)
this.status = 500
}
}

test('fails prechecks when the branch does not exist (deleted branch)', async () => {
// Mock getBranch to throw a 404 error for the PR branch check
octokit.rest.repos.getBranch = vi
.fn()
// First call: stable branch check (succeeds)
.mockReturnValueOnce({
data: {
commit: {sha: 'deadbeef', commit: {tree: {sha: 'beefdead'}}},
name: 'main'
},
status: 200
})
// Second call: base branch check (succeeds)
.mockReturnValueOnce({
data: {commit: {sha: 'deadbeef'}, name: 'main'},
status: 200
})
// Third call: PR branch check (fails with 404)
.mockRejectedValueOnce(new NotFoundError('Reference does not exist'))

const result = await prechecks(context, octokit, data)

expect(result.status).toBe(false)
expect(result.message).toContain('Cannot proceed with deployment')
expect(result.message).toContain('ref: `test-ref`')
expect(result.message).toContain(
'The branch for this pull request no longer exists'
)
expect(warningMock).toHaveBeenCalledWith('branch does not exist: test-ref')
})

test('passes prechecks when branch exists (normal deployment)', async () => {
// Mock getBranch to succeed for all calls
octokit.rest.repos.getBranch = vi
.fn()
// First call: stable branch check
.mockReturnValueOnce({
data: {
commit: {sha: 'deadbeef', commit: {tree: {sha: 'beefdead'}}},
name: 'main'
},
status: 200
})
// Second call: base branch check
.mockReturnValueOnce({
data: {commit: {sha: 'deadbeef'}, name: 'main'},
status: 200
})
// Third call: PR branch check (succeeds)
.mockReturnValueOnce({
data: {commit: {sha: 'abc123'}, name: 'test-ref'},
status: 200
})

const result = await prechecks(context, octokit, data)

expect(result.status).toBe(true)
expect(result.ref).toBe('test-ref')
expect(debugMock).toHaveBeenCalledWith('checking if branch exists: test-ref')
expect(infoMock).toHaveBeenCalledWith('✅ branch exists: test-ref')
})

test('skips branch existence check when deploying to stable branch', async () => {
data.environmentObj.stable_branch_used = true

// Mock getBranch - should only be called twice (not three times)
octokit.rest.repos.getBranch = vi
.fn()
.mockReturnValueOnce({
data: {
commit: {sha: 'deadbeef', commit: {tree: {sha: 'beefdead'}}},
name: 'main'
},
status: 200
})
.mockReturnValueOnce({
data: {commit: {sha: 'deadbeef'}, name: 'main'},
status: 200
})

const result = await prechecks(context, octokit, data)

expect(result.status).toBe(true)
// Verify the branch existence check was skipped (only 2 getBranch calls, not 3)
expect(octokit.rest.repos.getBranch).toHaveBeenCalledTimes(2)
expect(debugMock).not.toHaveBeenCalledWith(
'checking if branch exists: test-ref'
)
})

test('skips branch existence check when deploying an exact SHA', async () => {
data.environmentObj.sha = 'abc123def456'
data.inputs.allow_sha_deployments = true

octokit.rest.repos.getBranch = vi
.fn()
.mockReturnValueOnce({
data: {
commit: {sha: 'deadbeef', commit: {tree: {sha: 'beefdead'}}},
name: 'main'
},
status: 200
})
.mockReturnValueOnce({
data: {commit: {sha: 'deadbeef'}, name: 'main'},
status: 200
})

const result = await prechecks(context, octokit, data)

expect(result.status).toBe(true)
// Verify the branch existence check was skipped
expect(octokit.rest.repos.getBranch).toHaveBeenCalledTimes(2)
expect(debugMock).not.toHaveBeenCalledWith(
'checking if branch exists: test-ref'
)
})

test('skips branch existence check when PR is from a fork', async () => {
// Mock the PR as a fork
octokit.rest.pulls.get = vi.fn().mockReturnValue({
data: {
head: {
ref: 'test-ref',
sha: 'abc123',
repo: {
fork: true
},
label: 'fork:test-ref'
},
base: {
ref: 'main'
},
draft: false
},
status: 200
})

octokit.rest.repos.getBranch = vi
.fn()
.mockReturnValueOnce({
data: {
commit: {sha: 'deadbeef', commit: {tree: {sha: 'beefdead'}}},
name: 'main'
},
status: 200
})
.mockReturnValueOnce({
data: {commit: {sha: 'deadbeef'}, name: 'main'},
status: 200
})

const result = await prechecks(context, octokit, data)

expect(result.status).toBe(true)
expect(result.isFork).toBe(true)
// Verify the branch existence check was skipped for forks
expect(octokit.rest.repos.getBranch).toHaveBeenCalledTimes(2)
expect(debugMock).not.toHaveBeenCalledWith(
'checking if branch exists: abc123'
)
})

test('fails prechecks when branch check encounters unexpected error', async () => {
// Mock getBranch to throw a non-404 error
octokit.rest.repos.getBranch = vi
.fn()
.mockReturnValueOnce({
data: {
commit: {sha: 'deadbeef', commit: {tree: {sha: 'beefdead'}}},
name: 'main'
},
status: 200
})
.mockReturnValueOnce({
data: {commit: {sha: 'deadbeef'}, name: 'main'},
status: 200
})
.mockRejectedValueOnce(new UnexpectedError('Internal server error'))

const result = await prechecks(context, octokit, data)

// Should fail and not continue
expect(result.status).toBe(false)
expect(result.message).toContain('Cannot proceed with deployment')
expect(result.message).toContain('ref: `test-ref`')
expect(result.message).toContain(
'An unexpected error occurred while checking if the branch exists'
)
expect(result.message).toContain('Internal server error')
})
31 changes: 31 additions & 0 deletions dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions src/functions/prechecks.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,37 @@ export async function prechecks(context, octokit, data) {
core.saveState('review_decision', reviewDecision)
core.saveState('approved_reviews_count', approvedReviewsCount)

// Check if the branch exists before proceeding with deployment
// Skip this check if:
// 1. We're deploying to the stable branch (e.g., `.deploy main`)
// 2. We're deploying an exact SHA (allow_sha_deployments is enabled and a SHA was provided)
// 3. The PR is from a fork (we use SHA for forks, not branch names)
if (
data.environmentObj.stable_branch_used !== true &&
data.environmentObj.sha === null &&
isFork === false
) {
core.debug(`checking if branch exists: ${ref}`)
try {
await octokit.rest.repos.getBranch({
...context.repo,
branch: ref,
headers: API_HEADERS
})
core.info(`✅ branch exists: ${ref}`)
} catch (error) {
if (error.status === 404) {
message = `### ⚠️ Cannot proceed with deployment\n\n- ref: \`${ref}\`\n\nThe branch for this pull request no longer exists. This can happen if the branch was deleted after the PR was merged or closed. If you need to deploy, you can:\n- Use the stable branch deployment (e.g., \`${data.inputs.trigger} ${data.inputs.stable_branch}\`)\n- Use an exact SHA deployment if enabled (e.g., \`${data.inputs.trigger} ${sha}\`)\n\n> If you are running this command on a closed pull request, you can also try reopening the pull request to restore the branch for a deployment.`
core.warning(`branch does not exist: ${ref}`)
return {message: message, status: false}
}
// If it's not a 404 error, it's unexpected - hard stop
message = `### ⚠️ Cannot proceed with deployment\n\n- ref: \`${ref}\`\n\n> An unexpected error occurred while checking if the branch exists: \`${error.message}\``
core.error(`unexpected error checking if branch exists: ${error.message}`)
return {message: message, status: false}
}
}

// Always allow deployments to the "stable" branch regardless of CI checks or PR review
if (data.environmentObj.stable_branch_used === true) {
message = `✅ deployment to the ${COLORS.highlight}stable${COLORS.reset} branch requested`
Expand Down