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
169 changes: 168 additions & 1 deletion __tests__/tags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import {validateRegex} from '../src/pr-collector/regexUtils.js'
import {filterTags, prepareAndSortTags, TagInfo, Tags, transformTags} from '../src/pr-collector/tags.js'
import {BaseRepository} from '../src/repositories/BaseRepository.js'
import {clear} from '../src/transform.js'
import {describe, expect, test} from 'vitest'
import * as core from '@actions/core'
import * as github from '@actions/github'
import {createCommandManager} from '../src/pr-collector/gitHelper.js'
import {describe, expect, test, vi} from 'vitest'

vi.mock('@actions/core', async importOriginal => ({...(await importOriginal<typeof import('@actions/core')>()), warning: vi.fn()}))

vi.mock('../src/pr-collector/gitHelper.js', () => ({
createCommandManager: vi.fn(async () => ({initialCommit: async () => 'c'.repeat(40), latestTag: async () => '0.191.0'}))
}))

clear()

Expand Down Expand Up @@ -187,6 +196,164 @@ test('Should transform tags correctly using the regex', async () => {
}
})

const SHA_A = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const SHA_B = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
const INITIAL = 'cccccccccccccccccccccccccccccccccccccccc'

describe('findPredecessorTag', () => {
const tagsHelper = new Tags({} as BaseRepository)
const initial = {name: INITIAL, commit: INITIAL}

test('single tag differing from the current tag is the predecessor', async () => {
const tags: TagInfo[] = [{name: '0.191.0', commit: SHA_A}]
expect(await tagsHelper.findPredecessorTag(tags, '.', SHA_B, false)).toStrictEqual(tags[0])
})

test('single tag matching the current tag by name, commit or short sha falls back to the initial commit', async () => {
const tags: TagInfo[] = [{name: '0.191.0', commit: SHA_A}]
for (const current of ['0.191.0', '0.191.0'.toUpperCase(), SHA_A, SHA_A.toUpperCase(), SHA_A.substring(0, 7)]) {
expect(await tagsHelper.findPredecessorTag(tags, '.', current, false)).toStrictEqual(initial)
}
})

test('short sha below 7 chars is not treated as a commit match', async () => {
const tags: TagInfo[] = [{name: '0.191.0', commit: SHA_A}]
expect(await tagsHelper.findPredecessorTag(tags, '.', 'aaaaaa', false)).toStrictEqual(tags[0])
})

test('tag without commit only matches by name', async () => {
const tags: TagInfo[] = [{name: '1.0.0'}]
expect(await tagsHelper.findPredecessorTag(tags, '.', SHA_A, false)).toStrictEqual(tags[0])
})

test('no tags falls back to the initial commit', async () => {
expect(await tagsHelper.findPredecessorTag([], '.', SHA_A, false)).toStrictEqual(initial)
})

test('single pre-release tag is skipped with ignorePreReleases', async () => {
const tags: TagInfo[] = [{name: '1.0.0-rc1', commit: SHA_A, preRelease: true}]
expect(await tagsHelper.findPredecessorTag(tags, '.', SHA_B, true)).toStrictEqual(initial)
expect(await tagsHelper.findPredecessorTag(tags, '.', SHA_B, false)).toStrictEqual(tags[0])
})

const multi: TagInfo[] = [
{name: '2.0.0', commit: SHA_A},
{name: '1.1.0-rc1', commit: SHA_B, preRelease: true},
{name: '1.0.0', commit: 'dddddddddddddddddddddddddddddddddddddddd'}
]

test('multiple tags: match by name returns the next tag', async () => {
expect(await tagsHelper.findPredecessorTag(multi, '.', '2.0.0', false)).toStrictEqual(multi[1])
})

test('multiple tags: match by commit sha returns the next tag', async () => {
expect(await tagsHelper.findPredecessorTag(multi, '.', SHA_A, false)).toStrictEqual(multi[1])
})

test('multiple tags: ignorePreReleases skips pre-releases', async () => {
expect(await tagsHelper.findPredecessorTag(multi, '.', '2.0.0', true)).toStrictEqual(multi[2])
})

test('multiple tags: unmatched tag returns the newest tag', async () => {
expect(await tagsHelper.findPredecessorTag(multi, '.', 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', false)).toStrictEqual(multi[0])
})

test('multiple tags: unmatched tag with ignorePreReleases returns the newest release', async () => {
const tags: TagInfo[] = [{name: '3.0.0-rc1', preRelease: true}, multi[0]]
expect(await tagsHelper.findPredecessorTag(tags, '.', SHA_B, true)).toStrictEqual(multi[0])
})

test('multiple tags: oldest tag fails with a warning instead of assuming the initial commit', async () => {
expect(await tagsHelper.findPredecessorTag(multi, '.', '1.0.0', false)).toBeNull()
expect(core.warning).toHaveBeenCalledWith(expect.stringContaining("No tag found before '1.0.0'"))
})

test('multiple tags: matched tag with only older pre-releases keeps using the next tag', async () => {
const tags: TagInfo[] = [multi[0], multi[1]]
expect(await tagsHelper.findPredecessorTag(tags, '.', '2.0.0', true)).toStrictEqual(multi[1])
})

test('multiple tags: unmatched tag with only pre-releases falls back to the initial commit', async () => {
const tags: TagInfo[] = [{name: '3.0.0-rc1', preRelease: true}, multi[1]]
expect(await tagsHelper.findPredecessorTag(tags, '.', SHA_A, true)).toStrictEqual(initial)
})

test('returns null when resolving the initial commit fails', async () => {
vi.mocked(createCommandManager).mockRejectedValueOnce(new Error('no git'))
expect(await tagsHelper.findPredecessorTag([], '.', SHA_A, false)).toBeNull()
})
})

describe('retrieveRange with a single existing tag', () => {
const repo = {getTags: async () => [{name: '0.191.0', commit: SHA_A}]} as unknown as BaseRepository
const resolver: TagResolver = {method: 'semver'}

test('explicit toTag (untagged sha) uses the existing tag as fromTag', async () => {
const range = await new Tags(repo).retrieveRange('.', 'o', 'r', null, SHA_B, false, 1000, resolver)
expect(range.from?.name).toStrictEqual('0.191.0')
expect(range.to).toStrictEqual({name: SHA_B, commit: SHA_B})
})

test('explicit toTag equal to the existing tag falls back to the initial commit', async () => {
const range = await new Tags(repo).retrieveRange('.', 'o', 'r', null, '0.191.0', false, 1000, resolver)
expect(range.from).toStrictEqual({name: INITIAL, commit: INITIAL})
})

test('explicit fromTag is used as is', async () => {
const range = await new Tags(repo).retrieveRange('.', 'o', 'r', 'x', SHA_B, false, 1000, resolver)
expect(range.from).toStrictEqual({name: 'x', commit: 'x'})
})
})

describe('retrieveRange tag resolution', () => {
const resolver: TagResolver = {method: 'semver'}
const repo = (tags: TagInfo[]) =>
({
getTags: async () => tags,
fillTagInformation: async (_p: string, _o: string, _r: string, t: TagInfo) => t
}) as unknown as BaseRepository

test('toTag resolved from the tag ref of the context', async () => {
const ref = github.context.ref
github.context.ref = 'refs/tags/2.0.0'
try {
const range = await new Tags(repo([{name: '2.0.0'}, {name: '1.0.0'}])).retrieveRange('.', 'o', 'r', null, null, false, 1000, resolver)
expect(range.to).toStrictEqual({name: '2.0.0', commit: '2.0.0'})
expect(range.from?.name).toStrictEqual('1.0.0')
} finally {
github.context.ref = ref
}
})

test('toTag resolved from the newest tag when multiple tags exist', async () => {
const range = await new Tags(repo([{name: '1.0.0'}, {name: '2.0.0'}])).retrieveRange('.', 'o', 'r', null, null, false, 1000, resolver)
expect(range.to?.name).toStrictEqual('2.0.0')
expect(range.from?.name).toStrictEqual('1.0.0')
})

test('toTag resolved from git when a single tag exists, then falls back to the initial commit', async () => {
const range = await new Tags(repo([{name: '0.191.0', commit: SHA_A}])).retrieveRange('.', 'o', 'r', null, null, false, 1000, resolver)
expect(range.to?.name).toStrictEqual('0.191.0')
expect(range.from).toStrictEqual({name: INITIAL, commit: INITIAL})
})

test('filter and transformer path resolves a range with the original tag names', async () => {
const tags = [{name: 'api-1.0.0'}, {name: 'api-2.0.0'}, {name: 'other-3.0.0'}]
const range = await new Tags(repo(tags)).retrieveRange('.', 'o', 'r', null, 'api-2.0.0', false, 1000, {
method: 'semver',
filter: {pattern: '(api-.+)', target: '$1'},
transformer: [{pattern: 'api-(.+)', target: '$1'}]
})
expect(range.to?.name).toStrictEqual('api-2.0.0')
expect(range.from?.name).toStrictEqual('api-1.0.0')
})

test('fillTagInformation delegates to the repository', async () => {
const tag = {name: '1.0.0'}
expect(await new Tags(repo([])).fillTagInformation('.', 'o', 'r', tag)).toStrictEqual(tag)
})
})

describe('retrieveRange with a tag transformer', () => {
const repo = (tags: TagInfo[]) => ({getTags: async () => tags}) as unknown as BaseRepository
const strip = (pattern: string) => ({pattern, target: '$1'})
Expand Down
50 changes: 27 additions & 23 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.

58 changes: 35 additions & 23 deletions src/pr-collector/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ export interface SortableTagInfo extends TagInfo {
tmp: string
}

function isSameTag(tagInfo: TagInfo, tag: string): boolean {
const lowerTag = tag.toLocaleLowerCase('en')
const commit = tagInfo.commit?.toLocaleLowerCase('en')
// abbreviated shas (7+ chars) are matched by prefix
return (
tagInfo.name.toLocaleLowerCase('en') === lowerTag ||
(!!commit && (commit === lowerTag || (lowerTag.length >= 7 && commit.startsWith(lowerTag))))
)
}

export class Tags {
constructor(private repositoryUtils: BaseRepository) {}

Expand All @@ -43,30 +53,32 @@ export class Tags {
): Promise<TagInfo | null> {
const tags = sortedTags
try {
const length = tags.length
if (tags.length > 1) {
for (let i = 0; i < length; i++) {
if (tags[i].name.toLocaleLowerCase('en') === tag.toLocaleLowerCase('en')) {
if (ignorePreReleases) {
core.info(`ℹ️ Enabled 'ignorePreReleases', searching for the closest release`)
for (let ii = i + 1; ii < length; ii++) {
if (!tags[ii].preRelease) {
return tags[ii]
}
}
}
return tags[i + 1]
}
}
} else {
core.info(`ℹ️ Only one tag found for the given repository. Usually this is the case for the initial release.`)
// if not specified try to retrieve tag from git
const gitHelper = await createCommandManager(repositoryPath)
const initialCommit = await gitHelper.initialCommit()
core.info(`🔖 Resolved initial commit (${initialCommit}) from 'git rev-list --max-parents=0 HEAD'`)
return {name: initialCommit, commit: initialCommit}
// a tag matching none of the existing tags (e.g. the sha of a not yet tagged release) is newer than all of them
const current = tags.findIndex(t => isSameTag(t, tag))
if (ignorePreReleases) {
core.info(`ℹ️ Enabled 'ignorePreReleases', searching for the closest release`)
}
const older = tags.slice(current + 1)
// a matched tag keeps the previous behaviour of using the next tag, even if it is a pre-release
const predecessor = older.find(t => !ignorePreReleases || !t.preRelease) ?? (current >= 0 ? older[0] : undefined)
if (predecessor) {
return predecessor
}
return tags[0]

if (current >= 0 && tags.length > 1) {
// the matched tag is the oldest of several, it is unknown whether it is the first release or the tag list was truncated (`maxTagsToFetch`)
core.warning(
`⚠️ No tag found before '${tags[current].name}'. Not assuming the initial commit, as the list of tags may be truncated (see 'max_tags_to_fetch'). Specify 'fromTag' explicitly.`
)
return null
}

core.info(`ℹ️ No earlier tag found for the given repository. Usually this is the case for the initial release.`)
// if not specified try to retrieve tag from git
const gitHelper = await createCommandManager(repositoryPath)
const initialCommit = await gitHelper.initialCommit()
core.info(`🔖 Resolved initial commit (${initialCommit}) from 'git rev-list --max-parents=0 HEAD'`)
return {name: initialCommit, commit: initialCommit}
} catch {
if (tags.length <= 0) {
core.warning(`⚠️ No tag found for the given repository`)
Expand Down
Loading