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
63 changes: 50 additions & 13 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.

254 changes: 251 additions & 3 deletions src/autodev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,40 @@ vi.mock('@actions/core', () => ({
debug: vi.fn(),
getInput: vi.fn(),
info: vi.fn(),
setFailed: vi.fn()
setFailed: vi.fn(),
warning: vi.fn()
}))

vi.mock('@actions/exec', () => ({
exec: vi.fn()
}))

import {getInput, info} from '@actions/core'
import {getInput, info, setFailed, warning} from '@actions/core'
import {exec} from '@actions/exec'

import autoDev from './autodev'
import type {PullsListResponseData} from './utils'
import * as utils from './utils'

const REMOTE_DEV_SHA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const REMOTE_HEAD_SHA = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'

const stubRefSpecificExec = (extra?: Record<string, string>): void => {
const refMap: Record<string, string | undefined> = {
'git ls-remote --heads origin dev': `${REMOTE_DEV_SHA}\trefs/heads/dev`,
'git rev-parse origin/dev': REMOTE_DEV_SHA,
'git rev-parse HEAD': REMOTE_HEAD_SHA,
...extra
}
vi.mocked(exec).mockImplementation((cmd, _args, opts) => {
const out = refMap[cmd]
if (out) {
opts?.listeners?.stdout?.(Buffer.from(`${out}\n`))
}
return Promise.resolve(0)
})
}

describe('autodev', () => {
const labelsSpy = vi.spyOn(utils, 'updateLabels').mockResolvedValue()
const commentsSpy = vi.spyOn(utils, 'createComments').mockResolvedValue()
Expand Down Expand Up @@ -53,7 +73,7 @@ describe('autodev', () => {
}
] as PullsListResponseData)

vi.mocked(exec).mockResolvedValue(0)
stubRefSpecificExec()
})

afterEach(() => vi.clearAllMocks())
Expand Down Expand Up @@ -95,4 +115,232 @@ The following branches failed to merge:

expect(labelsSpy).toHaveBeenCalled()
})

it('should push with --force-with-lease pinned to the observed remote sha', async () => {
vi.mocked(getInput).mockImplementation(
input => ({token: 'token', base: 'main'})[input] || ''
)

await autoDev()

expect(exec).toHaveBeenCalledWith(
`git push --force-with-lease=refs/heads/dev:${REMOTE_DEV_SHA} -u origin refs/heads/dev`,
undefined,
expect.objectContaining({ignoreReturnCode: true})
)
expect(exec).not.toHaveBeenCalledWith(
'git push -f -u origin refs/heads/dev',
undefined,
expect.anything()
)
})
Comment thread
0x46616c6b marked this conversation as resolved.

it('should warn but not fail when the push is rejected because origin/dev moved', async () => {
vi.mocked(getInput).mockImplementation(
input => ({token: 'token', base: 'main'})[input] || ''
)

vi.mocked(exec).mockImplementation((cmd, _args, opts) => {
if (cmd === 'git ls-remote --heads origin dev') {
opts?.listeners?.stdout?.(
Buffer.from(`${REMOTE_DEV_SHA}\trefs/heads/dev\n`)
)
return Promise.resolve(0)
}
if (cmd === 'git rev-parse origin/dev') {
opts?.listeners?.stdout?.(Buffer.from(`${REMOTE_DEV_SHA}\n`))
return Promise.resolve(0)
}
if (cmd === 'git rev-parse HEAD') {
opts?.listeners?.stdout?.(Buffer.from(`${REMOTE_HEAD_SHA}\n`))
return Promise.resolve(0)
}
if (cmd.startsWith('git push --force-with-lease')) {
opts?.listeners?.stderr?.(
Buffer.from(
"! [rejected] dev -> dev (stale info)\nerror: failed to push some refs to 'origin'\n"
)
)
return Promise.resolve(1)
}
return Promise.resolve(0)
})

await autoDev()

expect(warning).toHaveBeenCalledWith(
expect.stringContaining(
`push to dev skipped: origin/dev moved during this run`
)
)
expect(setFailed).not.toHaveBeenCalled()
})

it('should setFailed with a generic message when the push fails for non-lease reasons', async () => {
vi.mocked(getInput).mockImplementation(
input => ({token: 'token', base: 'main'})[input] || ''
)

vi.mocked(exec).mockImplementation((cmd, _args, opts) => {
if (cmd === 'git ls-remote --heads origin dev') {
opts?.listeners?.stdout?.(
Buffer.from(`${REMOTE_DEV_SHA}\trefs/heads/dev\n`)
)
return Promise.resolve(0)
}
if (cmd === 'git rev-parse origin/dev') {
opts?.listeners?.stdout?.(Buffer.from(`${REMOTE_DEV_SHA}\n`))
return Promise.resolve(0)
}
if (cmd === 'git rev-parse HEAD') {
opts?.listeners?.stdout?.(Buffer.from(`${REMOTE_HEAD_SHA}\n`))
return Promise.resolve(0)
}
if (cmd.startsWith('git push --force-with-lease')) {
opts?.listeners?.stderr?.(
Buffer.from('fatal: unable to access remote: connection refused\n')
)
return Promise.resolve(128)
}
return Promise.resolve(0)
})

await autoDev()

expect(setFailed).toHaveBeenCalledWith(
expect.stringContaining('push to dev failed with exit code 128')
)
expect(setFailed).toHaveBeenCalledWith(
expect.stringContaining('connection refused')
)
})

it('should pin --force-with-lease to the start-of-run sha even after a second fetch advances origin/dev', async () => {
vi.mocked(getInput).mockImplementation(
input => ({token: 'token', base: 'main'})[input] || ''
)

const STALE_DEV_SHA = REMOTE_DEV_SHA
const ADVANCED_DEV_SHA = 'cccccccccccccccccccccccccccccccccccccccc'

let lsRemoteCalls = 0
let revParseOriginCalls = 0
vi.mocked(exec).mockImplementation((cmd, _args, opts) => {
if (cmd === 'git ls-remote --heads origin dev') {
lsRemoteCalls += 1
opts?.listeners?.stdout?.(
Buffer.from(`${STALE_DEV_SHA}\trefs/heads/dev\n`)
)
return Promise.resolve(0)
}
if (cmd === 'git rev-parse origin/dev') {
// After the second `git fetch`, the local origin/dev now points at the
// newer remote tip. The lease must NOT adopt this value.
revParseOriginCalls += 1
opts?.listeners?.stdout?.(Buffer.from(`${ADVANCED_DEV_SHA}\n`))
return Promise.resolve(0)
}
if (cmd === 'git rev-parse HEAD') {
opts?.listeners?.stdout?.(Buffer.from(`${REMOTE_HEAD_SHA}\n`))
return Promise.resolve(0)
}
return Promise.resolve(0)
})

await autoDev()

expect(lsRemoteCalls).toBeGreaterThanOrEqual(1)
expect(revParseOriginCalls).toBeGreaterThanOrEqual(1)
expect(exec).toHaveBeenCalledWith(
`git push --force-with-lease=refs/heads/dev:${STALE_DEV_SHA} -u origin refs/heads/dev`,
undefined,
expect.objectContaining({ignoreReturnCode: true})
)
expect(exec).not.toHaveBeenCalledWith(
`git push --force-with-lease=refs/heads/dev:${ADVANCED_DEV_SHA} -u origin refs/heads/dev`,
undefined,
expect.anything()
)
})

it('should not post comments or labels when the push is rejected', async () => {
vi.mocked(getInput).mockImplementation(
input =>
({token: 'token', base: 'main', comments: 'true', labels: 'true'})[
input
] || ''
)

vi.mocked(exec).mockImplementation((cmd, _args, opts) => {
if (cmd === 'git ls-remote --heads origin dev') {
opts?.listeners?.stdout?.(
Buffer.from(`${REMOTE_DEV_SHA}\trefs/heads/dev\n`)
)
return Promise.resolve(0)
}
if (cmd === 'git rev-parse origin/dev') {
opts?.listeners?.stdout?.(Buffer.from(`${REMOTE_DEV_SHA}\n`))
return Promise.resolve(0)
}
if (cmd === 'git rev-parse HEAD') {
opts?.listeners?.stdout?.(Buffer.from(`${REMOTE_HEAD_SHA}\n`))
return Promise.resolve(0)
}
if (cmd.startsWith('git push --force-with-lease')) {
opts?.listeners?.stderr?.(
Buffer.from('! [rejected] dev -> dev (stale info)\n')
)
return Promise.resolve(1)
}
return Promise.resolve(0)
})

await autoDev()

expect(commentsSpy).not.toHaveBeenCalled()
expect(labelsSpy).not.toHaveBeenCalled()
})

it('should post comments and labels only after a successful push', async () => {
vi.mocked(getInput).mockImplementation(
input =>
({token: 'token', base: 'main', comments: 'true', labels: 'true'})[
input
] || ''
)

const callOrder: string[] = []
vi.mocked(exec).mockImplementation((cmd, _args, opts) => {
if (cmd === 'git ls-remote --heads origin dev') {
opts?.listeners?.stdout?.(
Buffer.from(`${REMOTE_DEV_SHA}\trefs/heads/dev\n`)
)
return Promise.resolve(0)
}
if (cmd === 'git rev-parse origin/dev') {
opts?.listeners?.stdout?.(Buffer.from(`${REMOTE_DEV_SHA}\n`))
return Promise.resolve(0)
}
if (cmd === 'git rev-parse HEAD') {
opts?.listeners?.stdout?.(Buffer.from(`${REMOTE_HEAD_SHA}\n`))
return Promise.resolve(0)
}
if (cmd.startsWith('git push --force-with-lease')) {
callOrder.push('push')
}
return Promise.resolve(0)
})
commentsSpy.mockImplementation(() => {
callOrder.push('comment')
return Promise.resolve()
})
labelsSpy.mockImplementation(() => {
callOrder.push('label')
return Promise.resolve()
})

await autoDev()

expect(callOrder).toEqual(['push', 'comment', 'label'])
})
})
Loading
Loading