feat(inputs): add dry run support (#76)

This commit is contained in:
2025-12-23 21:31:53 +00:00
committed by GitHub
parent bb3e44be2f
commit ef41989077
11 changed files with 642 additions and 67 deletions

View File

@@ -4,7 +4,9 @@ import { jest } from '@jest/globals'
export const debug = jest.fn<typeof core.debug>()
export const error = jest.fn<typeof core.error>()
export const info = jest.fn<typeof core.info>()
export const notice = jest.fn<typeof core.notice>()
export const getInput = jest.fn<typeof core.getInput>()
export const getBooleanInput = jest.fn<typeof core.getBooleanInput>()
export const setOutput = jest.fn<typeof core.setOutput>()
export const setFailed = jest.fn<typeof core.setFailed>()
export const warning = jest.fn<typeof core.warning>()

193
tests/logger.test.ts Normal file
View File

@@ -0,0 +1,193 @@
/**
* Unit tests for the logger module, src/logger.ts
*/
import { jest } from '@jest/globals'
import * as core from './fixtures/core.js'
jest.unstable_mockModule('@actions/core', () => core)
const { createLogger } = await import('../src/logger.js')
describe('createLogger', () => {
beforeEach(() => {
jest.resetAllMocks()
})
describe('without prefix', () => {
it('calls core.debug with the message', () => {
const log = createLogger()
log.debug('test debug message')
expect(core.debug).toHaveBeenCalledWith('test debug message')
})
it('calls core.info with the message', () => {
const log = createLogger()
log.info('test info message')
expect(core.info).toHaveBeenCalledWith('test info message')
})
it('calls core.notice with string message', () => {
const log = createLogger()
log.notice('test notice message')
expect(core.notice).toHaveBeenCalledWith('test notice message', undefined)
})
it('calls core.notice with Error unchanged', () => {
const log = createLogger()
const error = new Error('test error')
log.notice(error)
expect(core.notice).toHaveBeenCalledWith(error, undefined)
})
it('calls core.notice with properties', () => {
const log = createLogger()
const props = { title: 'Notice Title' }
log.notice('test notice', props)
expect(core.notice).toHaveBeenCalledWith('test notice', props)
})
it('calls core.warning with string message', () => {
const log = createLogger()
log.warning('test warning message')
expect(core.warning).toHaveBeenCalledWith(
'test warning message',
undefined
)
})
it('calls core.warning with Error unchanged', () => {
const log = createLogger()
const error = new Error('warning error')
log.warning(error)
expect(core.warning).toHaveBeenCalledWith(error, undefined)
})
it('calls core.warning with properties', () => {
const log = createLogger()
const props = { title: 'Warning Title' }
log.warning('test warning', props)
expect(core.warning).toHaveBeenCalledWith('test warning', props)
})
it('calls core.error with string message', () => {
const log = createLogger()
log.error('test error message')
expect(core.error).toHaveBeenCalledWith('test error message', undefined)
})
it('calls core.error with Error unchanged', () => {
const log = createLogger()
const error = new Error('error error')
log.error(error)
expect(core.error).toHaveBeenCalledWith(error, undefined)
})
it('calls core.error with properties', () => {
const log = createLogger()
const props = { title: 'Error Title' }
log.error('test error', props)
expect(core.error).toHaveBeenCalledWith('test error', props)
})
})
describe('with prefix', () => {
it('prefixes debug messages', () => {
const log = createLogger('[dry-run] ')
log.debug('test debug message')
expect(core.debug).toHaveBeenCalledWith('[dry-run] test debug message')
})
it('prefixes info messages', () => {
const log = createLogger('[dry-run] ')
log.info('test info message')
expect(core.info).toHaveBeenCalledWith('[dry-run] test info message')
})
it('prefixes notice string messages', () => {
const log = createLogger('[dry-run] ')
log.notice('test notice message')
expect(core.notice).toHaveBeenCalledWith(
'[dry-run] test notice message',
undefined
)
})
it('wraps notice Error with prefixed message and cause', () => {
const log = createLogger('[dry-run] ')
const original = new Error('notice error')
log.notice(original)
expect(core.notice).toHaveBeenCalledTimes(1)
const [wrapped, props] = core.notice.mock.calls[0]
expect(wrapped).toBeInstanceOf(Error)
expect((wrapped as Error).message).toBe('[dry-run] notice error')
expect((wrapped as Error).cause).toBe(original)
expect((wrapped as Error).stack).toBe(original.stack)
expect(props).toBeUndefined()
})
it('prefixes warning string messages', () => {
const log = createLogger('[dry-run] ')
log.warning('test warning message')
expect(core.warning).toHaveBeenCalledWith(
'[dry-run] test warning message',
undefined
)
})
it('wraps warning Error with prefixed message and cause', () => {
const log = createLogger('[dry-run] ')
const original = new Error('warning error')
log.warning(original)
expect(core.warning).toHaveBeenCalledTimes(1)
const [wrapped, props] = core.warning.mock.calls[0]
expect(wrapped).toBeInstanceOf(Error)
expect((wrapped as Error).message).toBe('[dry-run] warning error')
expect((wrapped as Error).cause).toBe(original)
expect((wrapped as Error).stack).toBe(original.stack)
expect(props).toBeUndefined()
})
it('prefixes error string messages', () => {
const log = createLogger('[dry-run] ')
log.error('test error message')
expect(core.error).toHaveBeenCalledWith(
'[dry-run] test error message',
undefined
)
})
it('wraps error Error with prefixed message and cause', () => {
const log = createLogger('[dry-run] ')
const original = new Error('error error')
log.error(original)
expect(core.error).toHaveBeenCalledTimes(1)
const [wrapped, props] = core.error.mock.calls[0]
expect(wrapped).toBeInstanceOf(Error)
expect((wrapped as Error).message).toBe('[dry-run] error error')
expect((wrapped as Error).cause).toBe(original)
expect((wrapped as Error).stack).toBe(original.stack)
expect(props).toBeUndefined()
})
it('preserves properties when prefixing', () => {
const log = createLogger('[test] ')
const props = { title: 'Test Title', file: 'test.ts', startLine: 10 }
log.warning('message with props', props)
expect(core.warning).toHaveBeenCalledWith(
'[test] message with props',
props
)
})
})
describe('with empty prefix', () => {
it('behaves the same as no prefix', () => {
const log = createLogger('')
log.info('test message')
expect(core.info).toHaveBeenCalledWith('test message')
})
})
})

View File

@@ -16,9 +16,14 @@ jest.unstable_mockModule('csv-parse/sync', () => csvParse)
const { run } = await import('../src/main.js')
// Helper functions for cleaner test setup
const setupInputs = (inputs: Record<string, string>): void => {
const setupInputs = (inputs: Record<string, string | boolean>): void => {
core.getInput.mockImplementation((name: string) => {
return inputs[name] || ''
const value = inputs[name]
return typeof value === 'string' ? value : ''
})
core.getBooleanInput.mockImplementation((name: string) => {
const value = inputs[name]
return typeof value === 'boolean' ? value : false
})
}
@@ -123,7 +128,7 @@ describe('run', () => {
})
expect(core.info).toHaveBeenCalledWith(
"Tag 'v1' does not exist, creating with commit SHA sha-abc123."
"Creating tag 'v1' at commit SHA sha-abc123."
)
expect(getOutputs()).toEqual({
created: ['v1', 'v1.0'],
@@ -155,7 +160,7 @@ describe('run', () => {
})
expect(core.info).toHaveBeenCalledWith(
"Tag 'v1' exists, updating to commit SHA sha-def456 (was sha-old123)."
"Updating tag 'v1' to commit SHA sha-def456 (was sha-old123)."
)
expect(getOutputs()).toEqual({
created: [],
@@ -1219,4 +1224,225 @@ describe('run', () => {
tags: ['v1']
})
})
describe('dry-run mode', () => {
it('logs planned creates without executing them', async () => {
setupInputs({
tags: 'v1,v1.0',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update',
dry_run: true
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
// Should NOT call createRef in dry-run mode
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
// Should log dry-run messages
expect(core.info).toHaveBeenCalledWith(
'[dry-run] Dry-run mode enabled, no changes will be made.'
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would create tag 'v1' at commit SHA sha-abc123."
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would create tag 'v1.0' at commit SHA sha-abc123."
)
// Outputs should be empty in dry-run mode
expect(getOutputs()).toEqual({
created: [],
updated: [],
skipped: [],
tags: []
})
})
it('logs planned updates without executing them', async () => {
setupInputs({
tags: 'v1',
ref: 'def456',
github_token: 'test-token',
when_exists: 'update',
dry_run: true
})
setupCommitResolver('sha-def456')
setupTagExistsForAll('sha-old123')
await run()
// Should NOT call updateRef in dry-run mode
expect(github.mockOctokit.rest.git.updateRef).not.toHaveBeenCalled()
// Should log dry-run messages
expect(core.info).toHaveBeenCalledWith(
'[dry-run] Dry-run mode enabled, no changes will be made.'
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would update tag 'v1' " +
'to commit SHA sha-def456 (was sha-old123).'
)
// Outputs should be empty in dry-run mode
expect(getOutputs()).toEqual({
created: [],
updated: [],
skipped: [],
tags: []
})
})
it('logs skipped tags with dry-run prefix', async () => {
setupInputs({
tags: 'v1',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update',
dry_run: true
})
setupCommitResolver('sha-abc123')
setupTagExistsForAll('sha-abc123')
await run()
expect(core.info).toHaveBeenCalledWith(
'[dry-run] Dry-run mode enabled, no changes will be made.'
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Tag 'v1' already exists with desired commit SHA sha-abc123."
)
expect(getOutputs()).toEqual({
created: [],
updated: [],
skipped: [],
tags: []
})
})
it('handles mixed operations in dry-run mode', async () => {
setupInputs({
tags: 'v1,v2,v3',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update',
dry_run: true
})
setupCommitResolver('sha-abc123')
// v1 exists with different SHA, v2 matches, v3 doesn't exist
github.mockOctokit.rest.git.getRef.mockImplementation(
async (args: unknown) => {
const { ref } = args as { ref: string }
if (ref === 'tags/v1') {
return {
data: {
ref: 'refs/tags/v1',
object: { sha: 'sha-old', type: 'commit' }
}
}
}
if (ref === 'tags/v2') {
return {
data: {
ref: 'refs/tags/v2',
object: { sha: 'sha-abc123', type: 'commit' }
}
}
}
throw { status: 404 }
}
)
await run()
// No actual operations should happen
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
expect(github.mockOctokit.rest.git.updateRef).not.toHaveBeenCalled()
// Should log all planned operations
expect(core.info).toHaveBeenCalledWith(
'[dry-run] Dry-run mode enabled, no changes will be made.'
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would update tag 'v1' to commit SHA sha-abc123 (was sha-old)."
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Tag 'v2' already exists with desired commit SHA sha-abc123."
)
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would create tag 'v3' at commit SHA sha-abc123."
)
expect(getOutputs()).toEqual({
created: [],
updated: [],
skipped: [],
tags: []
})
})
it('logs annotated tag creation in dry-run mode', async () => {
setupInputs({
tags: 'v1',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update',
annotation: 'Release v1',
dry_run: true
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
expect(github.mockOctokit.rest.git.createTag).not.toHaveBeenCalled()
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
expect(core.info).toHaveBeenCalledWith(
"[dry-run] Would create tag 'v1' at commit SHA sha-abc123 (annotated)."
)
expect(getOutputs()).toEqual({
created: [],
updated: [],
skipped: [],
tags: []
})
})
it('executes normally when dry_run is false', async () => {
setupInputs({
tags: 'v1',
ref: 'abc123',
github_token: 'test-token',
when_exists: 'update',
dry_run: false
})
setupCommitResolver('sha-abc123')
setupTagDoesNotExist()
await run()
// Should actually create the tag
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(1)
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
owner: 'test-owner',
repo: 'test-repo',
ref: 'refs/tags/v1',
sha: 'sha-abc123'
})
expect(getOutputs()).toEqual({
created: ['v1'],
updated: [],
skipped: [],
tags: ['v1']
})
})
})
})