mirror of
https://github.com/jimeh/update-tags-action.git
synced 2026-02-19 01:26:40 +00:00
feat(tag): add support for annotated tags and improved tag handling (#15)
This commit is contained in:
@@ -5,7 +5,9 @@ export const mockOctokit = {
|
||||
git: {
|
||||
getRef: jest.fn<(args: unknown) => Promise<unknown>>(),
|
||||
createRef: jest.fn<(args: unknown) => Promise<unknown>>(),
|
||||
updateRef: jest.fn<(args: unknown) => Promise<unknown>>()
|
||||
updateRef: jest.fn<(args: unknown) => Promise<unknown>>(),
|
||||
createTag: jest.fn<(args: unknown) => Promise<unknown>>(),
|
||||
getTag: jest.fn<(args: unknown) => Promise<unknown>>()
|
||||
},
|
||||
repos: {
|
||||
getCommit: jest.fn<(args: unknown) => Promise<unknown>>()
|
||||
|
||||
@@ -47,13 +47,17 @@ const setupTagDoesNotExist = (): void => {
|
||||
})
|
||||
}
|
||||
|
||||
const setupTagExists = (tagName: string, sha: string): void => {
|
||||
const setupTagExists = (
|
||||
tagName: string,
|
||||
sha: string,
|
||||
type: 'commit' | 'tag' = 'commit'
|
||||
): void => {
|
||||
github.mockOctokit.rest.git.getRef.mockImplementation(
|
||||
async (args: unknown) => {
|
||||
const { ref } = args as { ref: string }
|
||||
if (ref === `tags/${tagName}`) {
|
||||
return {
|
||||
data: { ref: `refs/tags/${tagName}`, object: { sha } }
|
||||
data: { ref: `refs/tags/${tagName}`, object: { sha, type } }
|
||||
}
|
||||
}
|
||||
throw { status: 404 }
|
||||
@@ -61,9 +65,12 @@ const setupTagExists = (tagName: string, sha: string): void => {
|
||||
)
|
||||
}
|
||||
|
||||
const setupTagExistsForAll = (sha: string): void => {
|
||||
const setupTagExistsForAll = (
|
||||
sha: string,
|
||||
type: 'commit' | 'tag' = 'commit'
|
||||
): void => {
|
||||
github.mockOctokit.rest.git.getRef.mockResolvedValue({
|
||||
data: { ref: 'refs/tags/v1', object: { sha } }
|
||||
data: { ref: 'refs/tags/v1', object: { sha, type } }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -102,14 +109,14 @@ describe('run', () => {
|
||||
})
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
"Tag 'v1' does not exist, creating with SHA sha-abc123."
|
||||
"Tag 'v1' does not exist, creating with commit SHA sha-abc123."
|
||||
)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v1', 'v1.0'])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', [])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('tags', ['v1', 'v1.0'])
|
||||
})
|
||||
|
||||
it('updates existing tags when SHA differs', async () => {
|
||||
it('updates existing tags when commit SHA differs', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'def456',
|
||||
@@ -131,14 +138,14 @@ describe('run', () => {
|
||||
})
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
"Tag 'v1' exists, updating to SHA sha-def456 (was sha-old123)."
|
||||
"Tag 'v1' exists, updating to commit SHA sha-def456 (was sha-old123)."
|
||||
)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', [])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('tags', ['v1'])
|
||||
})
|
||||
|
||||
it('skips updating when tag exists with same SHA', async () => {
|
||||
it('skips updating when tag exists with same commit SHA', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'abc123',
|
||||
@@ -153,7 +160,7 @@ describe('run', () => {
|
||||
expect(github.mockOctokit.rest.git.updateRef).not.toHaveBeenCalled()
|
||||
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
"Tag 'v1' already exists with desired SHA sha-abc123."
|
||||
"Tag 'v1' already exists with desired commit SHA sha-abc123."
|
||||
)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', [])
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', [])
|
||||
@@ -293,6 +300,40 @@ describe('run', () => {
|
||||
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fails fast when when_exists is fail and one of multiple tags exists', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1,v2,v3',
|
||||
ref: 'abc123',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'fail'
|
||||
})
|
||||
setupCommitResolver('sha-abc123')
|
||||
|
||||
// Only v2 exists
|
||||
github.mockOctokit.rest.git.getRef.mockImplementation(
|
||||
async (args: unknown) => {
|
||||
const { ref } = args as { ref: string }
|
||||
if (ref === 'tags/v2') {
|
||||
return {
|
||||
data: {
|
||||
ref: 'refs/tags/v2',
|
||||
object: { sha: 'sha-old123', type: 'commit' }
|
||||
}
|
||||
}
|
||||
}
|
||||
throw { status: 404 }
|
||||
}
|
||||
)
|
||||
|
||||
await run()
|
||||
|
||||
// Should fail during desired tags resolution (resolveDesiredTags() in
|
||||
// tags.ts), before any tags are created
|
||||
expect(core.setFailed).toHaveBeenCalledWith("Tag 'v2' already exists.")
|
||||
expect(github.mockOctokit.rest.git.updateRef).not.toHaveBeenCalled()
|
||||
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fails when when_exists has invalid value', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
@@ -326,7 +367,7 @@ describe('run', () => {
|
||||
await run()
|
||||
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Action failed with error')
|
||||
expect.stringContaining("Failed to check if tag 'v1' exists")
|
||||
)
|
||||
})
|
||||
|
||||
@@ -654,4 +695,408 @@ describe('run', () => {
|
||||
'v4'
|
||||
])
|
||||
})
|
||||
|
||||
it('creates annotated tags when annotation is provided', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1,v1.0',
|
||||
ref: 'abc123',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'update',
|
||||
annotation: 'Release v1.0'
|
||||
})
|
||||
setupCommitResolver('sha-abc123')
|
||||
setupTagDoesNotExist()
|
||||
|
||||
github.mockOctokit.rest.git.createTag.mockImplementation(
|
||||
async (args: unknown) => {
|
||||
const { tag } = args as { tag: string }
|
||||
return { data: { sha: `sha-tag-object-${tag}` } }
|
||||
}
|
||||
)
|
||||
|
||||
await run()
|
||||
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledTimes(2)
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
tag: 'v1',
|
||||
message: 'Release v1.0',
|
||||
object: 'sha-abc123',
|
||||
type: 'commit'
|
||||
})
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
tag: 'v1.0',
|
||||
message: 'Release v1.0',
|
||||
object: 'sha-abc123',
|
||||
type: 'commit'
|
||||
})
|
||||
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledTimes(2)
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
ref: 'refs/tags/v1',
|
||||
sha: 'sha-tag-object-v1'
|
||||
})
|
||||
expect(github.mockOctokit.rest.git.createRef).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
ref: 'refs/tags/v1.0',
|
||||
sha: 'sha-tag-object-v1.0'
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('created', ['v1', 'v1.0'])
|
||||
})
|
||||
|
||||
it('updates existing tags with annotation', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'def456',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'update',
|
||||
annotation: 'Updated release'
|
||||
})
|
||||
setupCommitResolver('sha-def456')
|
||||
setupTagExistsForAll('sha-old123')
|
||||
|
||||
github.mockOctokit.rest.git.createTag.mockResolvedValue({
|
||||
data: { sha: 'sha-tag-object-456' }
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
tag: 'v1',
|
||||
message: 'Updated release',
|
||||
object: 'sha-def456',
|
||||
type: 'commit'
|
||||
})
|
||||
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
ref: 'tags/v1',
|
||||
sha: 'sha-tag-object-456',
|
||||
force: true
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
})
|
||||
|
||||
it('creates lightweight tags when annotation is empty', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'abc123',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'update',
|
||||
annotation: ''
|
||||
})
|
||||
setupCommitResolver('sha-abc123')
|
||||
setupTagDoesNotExist()
|
||||
|
||||
await run()
|
||||
|
||||
expect(github.mockOctokit.rest.git.createTag).not.toHaveBeenCalled()
|
||||
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(core.setOutput).toHaveBeenCalledWith('created', ['v1'])
|
||||
})
|
||||
|
||||
it('updates lightweight tags when annotation is empty', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'def456',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'update',
|
||||
annotation: ''
|
||||
})
|
||||
setupCommitResolver('sha-def456')
|
||||
setupTagExistsForAll('sha-old123')
|
||||
|
||||
await run()
|
||||
|
||||
expect(github.mockOctokit.rest.git.createTag).not.toHaveBeenCalled()
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
ref: 'tags/v1',
|
||||
sha: 'sha-def456',
|
||||
force: true
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
})
|
||||
|
||||
it('detects and dereferences existing annotated tags', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'def456',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'update'
|
||||
})
|
||||
setupCommitResolver('sha-def456')
|
||||
// Tag exists as annotated tag (type: 'tag', sha is tag object SHA)
|
||||
setupTagExistsForAll('sha-tag-object-old', 'tag')
|
||||
|
||||
// Mock getTag to return the underlying commit SHA
|
||||
github.mockOctokit.rest.git.getTag.mockResolvedValue({
|
||||
data: {
|
||||
sha: 'sha-tag-object-old',
|
||||
object: { sha: 'sha-old-commit', type: 'commit' }
|
||||
}
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// Should have called getTag to dereference the annotated tag
|
||||
expect(github.mockOctokit.rest.git.getTag).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.getTag).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
tag_sha: 'sha-tag-object-old'
|
||||
})
|
||||
|
||||
// Should update because commit SHAs differ
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledTimes(1)
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
})
|
||||
|
||||
it('updates existing annotated tags with new annotated tags', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'def456',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'update',
|
||||
annotation: 'Updated release'
|
||||
})
|
||||
setupCommitResolver('sha-def456')
|
||||
// Tag exists as annotated tag
|
||||
setupTagExistsForAll('sha-tag-object-old', 'tag')
|
||||
|
||||
// Mock getTag to return the underlying commit SHA
|
||||
github.mockOctokit.rest.git.getTag.mockResolvedValue({
|
||||
data: {
|
||||
sha: 'sha-tag-object-old',
|
||||
object: { sha: 'sha-old-commit', type: 'commit' }
|
||||
}
|
||||
})
|
||||
|
||||
// Mock createTag for the new annotated tag
|
||||
github.mockOctokit.rest.git.createTag.mockResolvedValue({
|
||||
data: { sha: 'sha-tag-object-new' }
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// Should have called getTag to dereference the existing annotated tag
|
||||
expect(github.mockOctokit.rest.git.getTag).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.getTag).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
tag_sha: 'sha-tag-object-old'
|
||||
})
|
||||
|
||||
// Should create new annotated tag
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
tag: 'v1',
|
||||
message: 'Updated release',
|
||||
object: 'sha-def456',
|
||||
type: 'commit'
|
||||
})
|
||||
|
||||
// Should update to new tag object SHA
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
ref: 'tags/v1',
|
||||
sha: 'sha-tag-object-new',
|
||||
force: true
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
})
|
||||
|
||||
it('updates annotated tag to lightweight when annotation removed', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'abc123',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'update',
|
||||
annotation: '' // No annotation = lightweight tag
|
||||
})
|
||||
setupCommitResolver('sha-abc123')
|
||||
// Tag exists as annotated tag pointing to same commit
|
||||
setupTagExistsForAll('sha-tag-object', 'tag')
|
||||
|
||||
// Mock getTag to return the same commit SHA as target
|
||||
github.mockOctokit.rest.git.getTag.mockResolvedValue({
|
||||
data: {
|
||||
sha: 'sha-tag-object',
|
||||
message: 'Old annotation',
|
||||
object: { sha: 'sha-abc123', type: 'commit' }
|
||||
}
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// Should have called getTag to dereference
|
||||
expect(github.mockOctokit.rest.git.getTag).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Should update to remove annotation
|
||||
expect(github.mockOctokit.rest.git.createTag).not.toHaveBeenCalled()
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
ref: 'tags/v1',
|
||||
sha: 'sha-abc123',
|
||||
force: true
|
||||
})
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
})
|
||||
|
||||
it('skips when annotated tag has same commit and same annotation', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'abc123',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'update',
|
||||
annotation: 'Release v1'
|
||||
})
|
||||
setupCommitResolver('sha-abc123')
|
||||
setupTagExistsForAll('sha-tag-object', 'tag')
|
||||
|
||||
github.mockOctokit.rest.git.getTag.mockResolvedValue({
|
||||
data: {
|
||||
sha: 'sha-tag-object',
|
||||
message: 'Release v1', // Same annotation
|
||||
object: { sha: 'sha-abc123', type: 'commit' }
|
||||
}
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// Should have called getTag to dereference
|
||||
expect(github.mockOctokit.rest.git.getTag).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Should NOT update because both commit and annotation match
|
||||
expect(github.mockOctokit.rest.git.updateRef).not.toHaveBeenCalled()
|
||||
expect(github.mockOctokit.rest.git.createRef).not.toHaveBeenCalled()
|
||||
expect(core.setOutput).toHaveBeenCalledWith('tags', [])
|
||||
})
|
||||
|
||||
it('updates when annotated tag has same commit but different annotation', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'abc123',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'update',
|
||||
annotation: 'Updated release message'
|
||||
})
|
||||
setupCommitResolver('sha-abc123')
|
||||
setupTagExistsForAll('sha-tag-object-old', 'tag')
|
||||
|
||||
github.mockOctokit.rest.git.getTag.mockResolvedValue({
|
||||
data: {
|
||||
sha: 'sha-tag-object-old',
|
||||
message: 'Old release message', // Different annotation
|
||||
object: { sha: 'sha-abc123', type: 'commit' }
|
||||
}
|
||||
})
|
||||
|
||||
github.mockOctokit.rest.git.createTag.mockResolvedValue({
|
||||
data: { sha: 'sha-tag-object-new' }
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// Should have called getTag to check existing annotation
|
||||
expect(github.mockOctokit.rest.git.getTag).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Should create new annotated tag with new message
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
tag: 'v1',
|
||||
message: 'Updated release message',
|
||||
object: 'sha-abc123',
|
||||
type: 'commit'
|
||||
})
|
||||
|
||||
// Should update to new tag object
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
ref: 'tags/v1',
|
||||
sha: 'sha-tag-object-new',
|
||||
force: true
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
})
|
||||
|
||||
it('updates lightweight tag to annotated when annotation added', async () => {
|
||||
setupInputs({
|
||||
tags: 'v1',
|
||||
ref: 'abc123',
|
||||
github_token: 'test-token',
|
||||
when_exists: 'update',
|
||||
annotation: 'New annotation'
|
||||
})
|
||||
setupCommitResolver('sha-abc123')
|
||||
// Tag exists as lightweight tag pointing to same commit
|
||||
setupTagExistsForAll('sha-abc123', 'commit')
|
||||
|
||||
github.mockOctokit.rest.git.createTag.mockResolvedValue({
|
||||
data: { sha: 'sha-tag-object-new' }
|
||||
})
|
||||
|
||||
await run()
|
||||
|
||||
// Should NOT call getTag since existing tag is lightweight
|
||||
expect(github.mockOctokit.rest.git.getTag).not.toHaveBeenCalled()
|
||||
|
||||
// Should create annotated tag
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.createTag).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
tag: 'v1',
|
||||
message: 'New annotation',
|
||||
object: 'sha-abc123',
|
||||
type: 'commit'
|
||||
})
|
||||
|
||||
// Should update to tag object
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledTimes(1)
|
||||
expect(github.mockOctokit.rest.git.updateRef).toHaveBeenCalledWith({
|
||||
owner: 'test-owner',
|
||||
repo: 'test-repo',
|
||||
ref: 'tags/v1',
|
||||
sha: 'sha-tag-object-new',
|
||||
force: true
|
||||
})
|
||||
|
||||
expect(core.setOutput).toHaveBeenCalledWith('updated', ['v1'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,12 @@ inputs:
|
||||
'fail'.
|
||||
required: false
|
||||
default: "update"
|
||||
annotation:
|
||||
description: >-
|
||||
Optional annotation message for tags. If provided, creates annotated tags.
|
||||
If empty, creates lightweight tags.
|
||||
required: false
|
||||
default: ""
|
||||
github_token:
|
||||
description: "The GitHub token to use for authentication."
|
||||
required: false
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20" role="img" aria-label="Coverage: 100%"><title>Coverage: 100%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="43" height="20" fill="#4c1"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="835" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">100%</text><text x="835" y="140" transform="scale(.1)" fill="#fff" textLength="330">100%</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="116" height="20" role="img" aria-label="Coverage: 94.44%"><title>Coverage: 94.44%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">94.44%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">94.44%</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
6
dist/index.js
generated
vendored
6
dist/index.js
generated
vendored
File diff suppressed because one or more lines are too long
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -1,16 +1,18 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import { parseTagsInput, type Tag } from './tags.js'
|
||||
import { parse } from 'csv-parse/sync'
|
||||
|
||||
const WHEN_EXISTS_MODES = ['update', 'skip', 'fail'] as const
|
||||
export type WhenExistsMode = (typeof WHEN_EXISTS_MODES)[number]
|
||||
|
||||
export interface Inputs {
|
||||
tags: Tag[]
|
||||
tags: string[]
|
||||
defaultRef: string
|
||||
whenExists: WhenExistsMode
|
||||
annotation: string
|
||||
owner: string
|
||||
repo: string
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
token: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,25 +37,34 @@ function validateWhenExists(input: string): WhenExistsMode {
|
||||
*
|
||||
* @returns Parsed and validated inputs
|
||||
*/
|
||||
export async function getInputs(): Promise<Inputs> {
|
||||
export function getInputs(): Inputs {
|
||||
const tagsInput: string = core.getInput('tags', { required: true })
|
||||
const defaultRef: string = core.getInput('ref')
|
||||
const whenExistsInput = core.getInput('when_exists') || 'update'
|
||||
const whenExists = validateWhenExists(whenExistsInput)
|
||||
const annotation: string = core.getInput('annotation')
|
||||
const token: string = core.getInput('github_token', {
|
||||
required: true
|
||||
})
|
||||
|
||||
const octokit = github.getOctokit(token)
|
||||
const { owner, repo } = github.context.repo
|
||||
|
||||
const tags = await parseTagsInput(octokit, tagsInput, defaultRef, owner, repo)
|
||||
// Parse tags as CSV/newline delimited strings
|
||||
const tags = (
|
||||
parse(tagsInput, {
|
||||
delimiter: ',',
|
||||
trim: true,
|
||||
relax_column_count: true
|
||||
}) as string[][]
|
||||
).flat()
|
||||
|
||||
return {
|
||||
tags,
|
||||
defaultRef,
|
||||
whenExists,
|
||||
annotation,
|
||||
owner,
|
||||
repo,
|
||||
octokit
|
||||
token
|
||||
}
|
||||
}
|
||||
|
||||
21
src/main.ts
21
src/main.ts
@@ -1,6 +1,7 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import { getInputs } from './inputs.js'
|
||||
import { processTag } from './tags.js'
|
||||
import { resolveDesiredTags, processTag } from './tags.js'
|
||||
|
||||
/**
|
||||
* The main function for the action.
|
||||
@@ -11,7 +12,7 @@ export async function run(): Promise<void> {
|
||||
try {
|
||||
let inputs
|
||||
try {
|
||||
inputs = await getInputs()
|
||||
inputs = getInputs()
|
||||
} catch (error) {
|
||||
// For parsing/validation errors, pass message directly.
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
@@ -19,14 +20,26 @@ export async function run(): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
const { tags, whenExists, owner, repo, octokit } = inputs
|
||||
// Create GitHub API client
|
||||
const octokit = github.getOctokit(inputs.token)
|
||||
|
||||
let tags
|
||||
try {
|
||||
tags = await resolveDesiredTags(inputs, octokit)
|
||||
} catch (error) {
|
||||
// For tag resolution errors (ref resolution, tag existence checks), pass
|
||||
// message directly.
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
core.setFailed(message)
|
||||
return
|
||||
}
|
||||
|
||||
const created: string[] = []
|
||||
const updated: string[] = []
|
||||
|
||||
// Create or update all tags.
|
||||
for (const tag of tags) {
|
||||
const result = await processTag(tag, whenExists, owner, repo, octokit)
|
||||
const result = await processTag(tag, octokit)
|
||||
|
||||
if (result === 'failed') {
|
||||
return
|
||||
|
||||
447
src/tags.ts
447
src/tags.ts
@@ -1,39 +1,86 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import { parse } from 'csv-parse/sync'
|
||||
import type { Inputs, WhenExistsMode } from './inputs.js'
|
||||
|
||||
export interface Tag {
|
||||
export interface ExistingTagInfo {
|
||||
commitSHA: string
|
||||
isAnnotated: boolean
|
||||
annotation?: string
|
||||
}
|
||||
|
||||
export interface DesiredTag {
|
||||
name: string
|
||||
ref: string
|
||||
sha: string
|
||||
whenExists: WhenExistsMode
|
||||
annotation: string
|
||||
owner: string
|
||||
repo: string
|
||||
existing?: ExistingTagInfo
|
||||
}
|
||||
|
||||
export type TagResult = 'created' | 'updated' | 'skipped' | 'failed'
|
||||
|
||||
/**
|
||||
* Parse tags input string and resolve refs to SHAs.
|
||||
*
|
||||
* @param octokit - The GitHub API client
|
||||
* @param tagsInput - The raw tags input string
|
||||
* @param defaultRef - The default ref to use if not specified per-tag
|
||||
* @param owner - The repository owner
|
||||
* @param repo - The repository name
|
||||
* @returns Array of desired tags with resolved SHAs
|
||||
*/
|
||||
export async function parseTagsInput(
|
||||
octokit: ReturnType<typeof github.getOctokit>,
|
||||
tagsInput: string,
|
||||
defaultRef: string,
|
||||
owner: string,
|
||||
interface TagOperationContext {
|
||||
owner: string
|
||||
repo: string
|
||||
): Promise<Tag[]> {
|
||||
const parsedTags: string[] = (
|
||||
parse(tagsInput, {
|
||||
delimiter: ',',
|
||||
trim: true,
|
||||
relax_column_count: true
|
||||
}) as string[][]
|
||||
).flat()
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch information about an existing tag, dereferencing if annotated.
|
||||
*
|
||||
* @param ctx - Operation context
|
||||
* @param existing - The existing tag reference data
|
||||
* @returns Information about the existing tag
|
||||
*/
|
||||
async function fetchExistingTagInfo(
|
||||
ctx: TagOperationContext,
|
||||
existing: { data: { object: { sha: string; type: string } } }
|
||||
): Promise<ExistingTagInfo> {
|
||||
const existingObject = existing.data.object
|
||||
const isAnnotated = existingObject.type === 'tag'
|
||||
|
||||
if (!isAnnotated) {
|
||||
return {
|
||||
commitSHA: existingObject.sha,
|
||||
isAnnotated: false
|
||||
}
|
||||
}
|
||||
|
||||
// Dereference annotated tag to get underlying commit
|
||||
const tagObject = await ctx.octokit.rest.git.getTag({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
tag_sha: existingObject.sha
|
||||
})
|
||||
|
||||
return {
|
||||
commitSHA: tagObject.data.object.sha,
|
||||
isAnnotated: true,
|
||||
annotation: tagObject.data.message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve desired tag objects from inputs.
|
||||
*
|
||||
* @param inputs - The validated inputs containing tags, refs, and configuration
|
||||
* @param octokit - The GitHub API client
|
||||
* @returns Array of desired tags with resolved SHAs and configuration
|
||||
*/
|
||||
export async function resolveDesiredTags(
|
||||
inputs: Inputs,
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
): Promise<DesiredTag[]> {
|
||||
const {
|
||||
tags: parsedTags,
|
||||
defaultRef,
|
||||
whenExists,
|
||||
annotation,
|
||||
owner,
|
||||
repo
|
||||
} = inputs
|
||||
|
||||
const uniqueRefs = new Set<string>()
|
||||
const tags: Record<string, string> = {}
|
||||
@@ -60,7 +107,7 @@ export async function parseTagsInput(
|
||||
throw new Error("Missing ref: provide 'ref' input or specify per-tag ref")
|
||||
}
|
||||
|
||||
// Check for duplicate tag with different ref.
|
||||
// Check for duplicate tag with different ref
|
||||
if (tags[tagName] && tags[tagName] !== ref) {
|
||||
throw new Error(
|
||||
`Duplicate tag '${tagName}' with different refs: ` +
|
||||
@@ -73,112 +120,84 @@ export async function parseTagsInput(
|
||||
}
|
||||
|
||||
// Pre-resolve all unique refs in parallel.
|
||||
const ctx: TagOperationContext = { owner, repo, octokit }
|
||||
const refToSha: Record<string, string> = {}
|
||||
await Promise.all(
|
||||
Array.from(uniqueRefs).map(async (ref) => {
|
||||
refToSha[ref] = await resolveRefToSha(octokit, owner, repo, ref)
|
||||
refToSha[ref] = await resolveRefToSha(ctx, ref)
|
||||
})
|
||||
)
|
||||
|
||||
// Build result array with resolved SHAs.
|
||||
const result: Tag[] = []
|
||||
for (const tagName in tags) {
|
||||
const tagRef = tags[tagName]
|
||||
result.push({
|
||||
name: tagName,
|
||||
ref: tagRef,
|
||||
sha: refToSha[tagRef]
|
||||
// Build result array with resolved SHAs and check for existing tags.
|
||||
const tagNames = Object.keys(tags)
|
||||
const result: DesiredTag[] = await Promise.all(
|
||||
tagNames.map(async (tagName) => {
|
||||
const tagRef = tags[tagName]
|
||||
const sha = refToSha[tagRef]
|
||||
|
||||
// Check if tag already exists
|
||||
let existing: ExistingTagInfo | undefined
|
||||
try {
|
||||
const existingRef = await ctx.octokit.rest.git.getRef({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
ref: `tags/${tagName}`
|
||||
})
|
||||
existing = await fetchExistingTagInfo(ctx, existingRef)
|
||||
|
||||
// Fail early if when_exists is 'fail'
|
||||
if (whenExists === 'fail') {
|
||||
throw new Error(`Tag '${tagName}' already exists.`)
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// Check if it's a GitHub API error with a status property
|
||||
if (typeof error === 'object' && error !== null && 'status' in error) {
|
||||
const apiError = error as { status: number; message?: string }
|
||||
if (apiError.status === 404) {
|
||||
// Tag doesn't exist, existing remains undefined
|
||||
} else {
|
||||
// Some other API error
|
||||
throw new Error(
|
||||
`Failed to check if tag '${tagName}' exists: ${apiError.message || String(error)}`
|
||||
)
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
// Already an Error (e.g., from when_exists === 'fail')
|
||||
throw error
|
||||
} else {
|
||||
// Unknown error type
|
||||
throw new Error(
|
||||
`Failed to check if tag '${tagName}' exists: ${String(error)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: tagName,
|
||||
ref: tagRef,
|
||||
sha,
|
||||
whenExists,
|
||||
annotation,
|
||||
owner,
|
||||
repo,
|
||||
existing
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single desired tag: create or update it based on configuration.
|
||||
*
|
||||
* @param tag - The desired tag to process
|
||||
* @param whenExists - What to do if the tag already exists
|
||||
* @param owner - Repository owner
|
||||
* @param repo - Repository name
|
||||
* @param octokit - GitHub API client
|
||||
* @returns The result of the tag operation
|
||||
*/
|
||||
export async function processTag(
|
||||
tag: Tag,
|
||||
whenExists: 'update' | 'skip' | 'fail',
|
||||
owner: string,
|
||||
repo: string,
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
): Promise<TagResult> {
|
||||
const { name: tagName, sha } = tag
|
||||
|
||||
try {
|
||||
// Check if the tag exists.
|
||||
const existing = await octokit.rest.git.getRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `tags/${tagName}`
|
||||
})
|
||||
|
||||
// If the tag exists, decide action based on 'when_exists'.
|
||||
if (whenExists === 'update') {
|
||||
const existingSHA = existing.data.object.sha
|
||||
if (existingSHA === sha) {
|
||||
core.info(`Tag '${tagName}' already exists with desired SHA ${sha}.`)
|
||||
return 'skipped'
|
||||
}
|
||||
|
||||
core.info(
|
||||
`Tag '${tagName}' exists, updating to SHA ${sha} ` +
|
||||
`(was ${existingSHA}).`
|
||||
)
|
||||
await octokit.rest.git.updateRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `tags/${tagName}`,
|
||||
sha,
|
||||
force: true
|
||||
})
|
||||
return 'updated'
|
||||
} else if (whenExists === 'skip') {
|
||||
core.info(`Tag '${tagName}' exists, skipping.`)
|
||||
return 'skipped'
|
||||
} else {
|
||||
// whenExists === 'fail'
|
||||
core.setFailed(`Tag '${tagName}' already exists.`)
|
||||
return 'failed'
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const err = error as { status?: number }
|
||||
if (err?.status !== 404) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// If the tag doesn't exist (404), create it.
|
||||
core.info(`Tag '${tagName}' does not exist, creating with SHA ${sha}.`)
|
||||
await octokit.rest.git.createRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `refs/tags/${tagName}`,
|
||||
sha
|
||||
})
|
||||
return 'created'
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveRefToSha(
|
||||
octokit: ReturnType<typeof github.getOctokit>,
|
||||
owner: string,
|
||||
repo: string,
|
||||
ctx: TagOperationContext,
|
||||
ref: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const {
|
||||
data: { sha }
|
||||
} = await octokit.rest.repos.getCommit({
|
||||
owner,
|
||||
repo,
|
||||
} = await ctx.octokit.rest.repos.getCommit({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
ref
|
||||
})
|
||||
|
||||
@@ -187,3 +206,201 @@ async function resolveRefToSha(
|
||||
throw new Error(`Failed to resolve ref '${ref}' to a SHA: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single desired tag: create or update it based on configuration.
|
||||
*
|
||||
* @param tag - The desired tag to process (with existing info if applicable)
|
||||
* @param octokit - GitHub API client
|
||||
* @returns The result of the tag operation
|
||||
*/
|
||||
export async function processTag(
|
||||
tag: DesiredTag,
|
||||
octokit: ReturnType<typeof github.getOctokit>
|
||||
): Promise<TagResult> {
|
||||
const ctx: TagOperationContext = { owner: tag.owner, repo: tag.repo, octokit }
|
||||
|
||||
// Tag doesn't exist, create it
|
||||
if (!tag.existing) {
|
||||
return await createTag(ctx, tag)
|
||||
}
|
||||
|
||||
// Tag exists - handle based on when_exists strategy
|
||||
if (tag.whenExists === 'skip') {
|
||||
core.info(`Tag '${tag.name}' exists, skipping.`)
|
||||
return 'skipped'
|
||||
}
|
||||
|
||||
if (tag.whenExists === 'fail') {
|
||||
// This should not happen as we fail early in resolveDesiredTags
|
||||
core.setFailed(`Tag '${tag.name}' already exists.`)
|
||||
return 'failed'
|
||||
}
|
||||
|
||||
// whenExists === 'update' - check if update is needed
|
||||
if (tagMatchesTarget(tag)) {
|
||||
core.info(
|
||||
`Tag '${tag.name}' already exists with desired commit SHA ${tag.sha}` +
|
||||
(tag.existing.isAnnotated ? ' (annotated).' : '.')
|
||||
)
|
||||
return 'skipped'
|
||||
}
|
||||
|
||||
return await updateExistingTag(ctx, tag)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing tag to point to a new commit and/or annotation.
|
||||
*/
|
||||
async function updateExistingTag(
|
||||
ctx: TagOperationContext,
|
||||
tag: DesiredTag
|
||||
): Promise<TagResult> {
|
||||
if (!tag.existing) {
|
||||
throw new Error(`Cannot update non-existent tag '${tag.name}'`)
|
||||
}
|
||||
|
||||
const reasons = getUpdateReasons(tag)
|
||||
const commitMatches = tag.existing.commitSHA === tag.sha
|
||||
|
||||
if (commitMatches) {
|
||||
core.info(
|
||||
`Tag '${tag.name}' exists with same commit but ${reasons.join(', ')}.`
|
||||
)
|
||||
} else {
|
||||
core.info(
|
||||
`Tag '${tag.name}' exists` +
|
||||
`${tag.existing.isAnnotated ? ' (annotated)' : ''}` +
|
||||
`, updating to ${reasons.join(', ')}.`
|
||||
)
|
||||
}
|
||||
|
||||
const targetSha = await resolveTargetSHA(ctx, tag)
|
||||
|
||||
await ctx.octokit.rest.git.updateRef({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
ref: `tags/${tag.name}`,
|
||||
sha: targetSha,
|
||||
force: true
|
||||
})
|
||||
|
||||
return 'updated'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tag (doesn't exist yet).
|
||||
*/
|
||||
async function createTag(
|
||||
ctx: TagOperationContext,
|
||||
tag: DesiredTag
|
||||
): Promise<TagResult> {
|
||||
core.info(
|
||||
`Tag '${tag.name}' does not exist, creating with commit SHA ${tag.sha}.`
|
||||
)
|
||||
|
||||
const targetSha = await resolveTargetSHA(ctx, tag)
|
||||
|
||||
await ctx.octokit.rest.git.createRef({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
ref: `refs/tags/${tag.name}`,
|
||||
sha: targetSha
|
||||
})
|
||||
|
||||
return 'created'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the target SHA for a tag (creates annotated tag object if needed).
|
||||
*
|
||||
* @param ctx - Operation context
|
||||
* @param tag - The tag to create
|
||||
* @returns The SHA to use (tag object SHA if annotated, commit SHA otherwise)
|
||||
*/
|
||||
async function resolveTargetSHA(
|
||||
ctx: TagOperationContext,
|
||||
tag: DesiredTag
|
||||
): Promise<string> {
|
||||
if (!tag.annotation) {
|
||||
return tag.sha
|
||||
}
|
||||
|
||||
const tagObject = await ctx.octokit.rest.git.createTag({
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
tag: tag.name,
|
||||
message: tag.annotation,
|
||||
object: tag.sha,
|
||||
type: 'commit'
|
||||
})
|
||||
|
||||
return tagObject.data.sha
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare existing tag state with desired target state.
|
||||
*
|
||||
* @param tag - The desired tag with existing info
|
||||
* @returns Object indicating whether commit and annotation match
|
||||
*/
|
||||
function compareTagState(tag: DesiredTag): {
|
||||
commitMatches: boolean
|
||||
annotationMatches: boolean
|
||||
} {
|
||||
if (!tag.existing) {
|
||||
return { commitMatches: false, annotationMatches: false }
|
||||
}
|
||||
|
||||
const commitMatches = tag.existing.commitSHA === tag.sha
|
||||
const annotationMatches =
|
||||
tag.existing.isAnnotated && tag.annotation
|
||||
? tag.existing.annotation === tag.annotation
|
||||
: !tag.existing.isAnnotated && !tag.annotation
|
||||
|
||||
return { commitMatches, annotationMatches }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag needs to be updated based on commit and annotation.
|
||||
*
|
||||
* @param tag - The desired tag with existing info
|
||||
* @returns True if the tag matches the target state
|
||||
*/
|
||||
function tagMatchesTarget(tag: DesiredTag): boolean {
|
||||
const { commitMatches, annotationMatches } = compareTagState(tag)
|
||||
return commitMatches && annotationMatches
|
||||
}
|
||||
|
||||
/**
|
||||
* Get update reason messages based on what changed.
|
||||
*
|
||||
* @param tag - The desired tag with existing info
|
||||
* @returns Array of reason strings
|
||||
*/
|
||||
function getUpdateReasons(tag: DesiredTag): string[] {
|
||||
if (!tag.existing) return []
|
||||
|
||||
const { commitMatches, annotationMatches } = compareTagState(tag)
|
||||
const reasons: string[] = []
|
||||
|
||||
if (!commitMatches) {
|
||||
reasons.push(`commit SHA ${tag.sha} (was ${tag.existing.commitSHA})`)
|
||||
}
|
||||
|
||||
if (!annotationMatches && tag.annotation) {
|
||||
if (tag.existing.isAnnotated) {
|
||||
reasons.push('annotation message changed')
|
||||
} else {
|
||||
reasons.push('adding annotation')
|
||||
}
|
||||
} else if (
|
||||
!annotationMatches &&
|
||||
!tag.annotation &&
|
||||
tag.existing.isAnnotated
|
||||
) {
|
||||
reasons.push('removing annotation')
|
||||
}
|
||||
|
||||
return reasons
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user