From fc6896090261dd66187a9f32cc37fa1522fe2070 Mon Sep 17 00:00:00 2001 From: erodozer Date: Fri, 27 Dec 2024 14:33:19 -0500 Subject: [PATCH] Support updating existing tag ref --- action.yml | 4 ++ src/action.ts | 17 +++++--- src/github.ts | 27 +++++++++---- src/utils.ts | 13 ++----- tests/action.test.ts | 93 ++++++++++++++++++++++++++++++++++++++------ tests/helper.test.ts | 8 ++++ tests/utils.test.ts | 18 ++------- 7 files changed, 132 insertions(+), 48 deletions(-) diff --git a/action.yml b/action.yml index 7b8bb1da8..ca6219456 100644 --- a/action.yml +++ b/action.yml @@ -36,6 +36,10 @@ inputs: custom_tag: description: "Custom tag name. If specified, it overrides bump settings." required: false + force_update: + description: "Updates the sha of a tag if it already exists" + required: false + default: "false" custom_release_rules: description: "Comma separated list of release rules. Format: `:`. Example: `hotfix:patch,pre-feat:preminor`." required: false diff --git a/src/action.ts b/src/action.ts index 2a073169e..1258a5c50 100644 --- a/src/action.ts +++ b/src/action.ts @@ -12,7 +12,7 @@ import { mapCustomReleaseRules, mergeWithDefaultChangelogRules, } from './utils'; -import { createTag } from './github'; +import { createTag, listTags } from './github'; import { Await } from './ts'; export default async function main() { @@ -22,6 +22,9 @@ export default async function main() { | 'false'; const tagPrefix = core.getInput('tag_prefix'); const customTag = core.getInput('custom_tag'); + const forceUpdate = /true/i.test( + core.getInput('force_update') + ); const releaseBranches = core.getInput('release_branches'); const preReleaseBranches = core.getInput('pre_release_branches'); const appendToPreReleaseTag = core.getInput('append_to_pre_release_tag'); @@ -69,10 +72,13 @@ export default async function main() { const prefixRegex = new RegExp(`^${tagPrefix}`); - const validTags = await getValidTags( - prefixRegex, + const tags = await listTags( /true/i.test(shouldFetchAllTags) ); + const validTags = await getValidTags( + tags, + prefixRegex + ); const latestTag = getLatestTag(validTags, prefixRegex, tagPrefix); const latestPrereleaseTag = getLatestPrereleaseTag( validTags, @@ -218,7 +224,8 @@ export default async function main() { return; } - if (validTags.map((tag) => tag.name).includes(newTag)) { + const tagExists = tags.map((tag) => tag.name).includes(newTag); + if (tagExists && !forceUpdate) { core.info('This tag already exists. Skipping the tag creation.'); return; } @@ -228,5 +235,5 @@ export default async function main() { return; } - await createTag(newTag, createAnnotatedTag, commitRef); + await createTag(newTag, createAnnotatedTag, tagExists, commitRef); } diff --git a/src/github.ts b/src/github.ts index 908a6996e..77cc79c8f 100644 --- a/src/github.ts +++ b/src/github.ts @@ -4,7 +4,7 @@ import { Await } from './ts'; let octokitSingleton: ReturnType; -type Tag = { +export type Tag = { name: string; commit: { sha: string; @@ -15,6 +15,8 @@ type Tag = { node_id: string; }; +export type Tags = Await>; + export function getOctokitSingleton() { if (octokitSingleton) { return octokitSingleton; @@ -68,6 +70,7 @@ export async function compareCommits(baseRef: string, headRef: string) { export async function createTag( newTag: string, createAnnotatedTag: boolean, + update: boolean, GITHUB_SHA: string ) { const octokit = getOctokitSingleton(); @@ -85,10 +88,20 @@ export async function createTag( }); } - core.debug(`Pushing new tag to the repo.`); - await octokit.git.createRef({ - ...context.repo, - ref: `refs/tags/${newTag}`, - sha: annotatedTag ? annotatedTag.data.sha : GITHUB_SHA, - }); + if (update) { + core.info(`Updating existing tag ${newTag} on the repo.`); + await octokit.git.updateRef({ + ...context.repo, + ref: `tags/${newTag}`, + sha: annotatedTag ? annotatedTag.data.sha : GITHUB_SHA, + force: true, + }); + } else { + core.info(`Pushing new tag to the repo.`); + await octokit.git.createRef({ + ...context.repo, + ref: `refs/tags/${newTag}`, + sha: annotatedTag ? annotatedTag.data.sha : GITHUB_SHA, + }); + } } diff --git a/src/utils.ts b/src/utils.ts index 8952c345e..8a54156b5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,24 +2,19 @@ import * as core from '@actions/core'; import { prerelease, rcompare, valid } from 'semver'; // @ts-ignore import DEFAULT_RELEASE_TYPES from '@semantic-release/commit-analyzer/lib/default-release-types'; -import { compareCommits, listTags } from './github'; +import { compareCommits, Tags } from './github'; import { defaultChangelogRules } from './defaults'; -import { Await } from './ts'; - -type Tags = Await>; export async function getValidTags( - prefixRegex: RegExp, - shouldFetchAllTags: boolean + tags: Tags, + prefixRegex: RegExp ) { - const tags = await listTags(shouldFetchAllTags); - const invalidTags = tags.filter( (tag) => !prefixRegex.test(tag.name) || !valid(tag.name.replace(prefixRegex, '')) ); - invalidTags.forEach((name) => core.debug(`Found Invalid Tag: ${name}.`)); + invalidTags.forEach((tag) => core.debug(`Found Invalid Tag: ${tag.name}.`)); const validTags = tags .filter( diff --git a/tests/action.test.ts b/tests/action.test.ts index 413e7bfae..2acbc4787 100644 --- a/tests/action.test.ts +++ b/tests/action.test.ts @@ -3,6 +3,7 @@ import * as utils from '../src/utils'; import * as github from '../src/github'; import * as core from '@actions/core'; import { + clearInputs, loadDefaultInputs, setBranch, setCommitSha, @@ -33,6 +34,7 @@ describe('github-tag-action', () => { jest.clearAllMocks(); setBranch('master'); setCommitSha('79e0ea271c26aa152beef77c3275ff7b8f8d8274'); + clearInputs(); loadDefaultInputs(); }); @@ -48,7 +50,7 @@ describe('github-tag-action', () => { const validTags: any[] = []; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -62,6 +64,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v0.0.1', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -78,7 +81,7 @@ describe('github-tag-action', () => { const validTags: any[] = []; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -92,6 +95,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v0.0.1', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -117,7 +121,7 @@ describe('github-tag-action', () => { }, ]; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -155,7 +159,7 @@ describe('github-tag-action', () => { }, ]; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -169,6 +173,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v2.0.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -197,9 +202,10 @@ describe('github-tag-action', () => { }, ]; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); + /* * When */ @@ -211,6 +217,55 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v1.3.0', expect.any(Boolean), + false, + expect.any(String) + ); + expect(mockSetFailed).not.toBeCalled(); + }); + + it('does update existing tag when force enabled', async () => { + /* + * Given + */ + setInput('force_update', 'true'); + setInput('custom_tag', 'latest'); + setInput('tag_prefix', ''); + const commits = [ + { + message: 'feat: some new feature on a pre-release branch', + hash: null, + }, + { message: 'james: this should make a preminor', hash: null }, + ]; + jest + .spyOn(utils, 'getCommits') + .mockImplementation(async (sha) => commits); + + const validTags = [ + { + name: 'latest', + commit: { sha: '012345', url: '' }, + zipball_url: '', + tarball_url: 'string', + node_id: 'string', + }, + ]; + jest + .spyOn(github, 'listTags') + .mockImplementation(async () => validTags); + + /* + * When + */ + await action(); + + /* + * Then + */ + expect(mockCreateTag).toHaveBeenCalledWith( + 'latest', + expect.any(Boolean), + true, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -243,7 +298,7 @@ describe('github-tag-action', () => { }, ]; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -257,6 +312,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v1.2.4', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -283,7 +339,7 @@ describe('github-tag-action', () => { }, ]; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -297,6 +353,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v1.3.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -327,7 +384,7 @@ describe('github-tag-action', () => { }, ]; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -341,6 +398,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v2.0.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -381,7 +439,7 @@ describe('github-tag-action', () => { }, ]; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -395,6 +453,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v2.2.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -426,7 +485,7 @@ describe('github-tag-action', () => { }, ]; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -440,6 +499,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v1.3.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -522,6 +582,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v1.2.4-prerelease.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -560,6 +621,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v1.2.4-prerelease.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -600,6 +662,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v1.3.0-prerelease.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -644,6 +707,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v2.0.0-prerelease.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -701,6 +765,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v2.2.0-prerelease.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -746,6 +811,7 @@ describe('github-tag-action', () => { expect(mockCreateTag).toHaveBeenCalledWith( 'v1.3.0-prerelease.0', expect.any(Boolean), + false, expect.any(String) ); expect(mockSetFailed).not.toBeCalled(); @@ -779,7 +845,7 @@ describe('github-tag-action', () => { }, ]; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -816,7 +882,7 @@ describe('github-tag-action', () => { }, ]; jest - .spyOn(utils, 'getValidTags') + .spyOn(github, 'listTags') .mockImplementation(async () => validTags); /* @@ -856,6 +922,9 @@ describe('github-tag-action', () => { node_id: 'string', }, ]; + jest + .spyOn(github, 'listTags') + .mockImplementation(async () => validTags); jest .spyOn(utils, 'getValidTags') .mockImplementation(async () => validTags); diff --git a/tests/helper.test.ts b/tests/helper.test.ts index d56b50be5..f67642fd8 100644 --- a/tests/helper.test.ts +++ b/tests/helper.test.ts @@ -26,6 +26,14 @@ export function setInputs(map: { [key: string]: string }) { Object.keys(map).forEach((key) => setInput(key, map[key])); } +export function clearInputs() { + Object.keys(process.env) + .filter(key => key.startsWith("INPUT_")) + .forEach( + key => delete process.env[key] + ); +} + export function loadDefaultInputs() { const actionYaml = fs.readFileSync( path.join(process.cwd(), 'action.yml'), diff --git a/tests/utils.test.ts b/tests/utils.test.ts index a305eb5a6..6f153479f 100644 --- a/tests/utils.test.ts +++ b/tests/utils.test.ts @@ -64,19 +64,15 @@ describe('utils', () => { node_id: 'string', }, ]; - const mockListTags = jest - .spyOn(github, 'listTags') - .mockImplementation(async () => testTags); /* * When */ - const validTags = await getValidTags(regex, false); + const validTags = await getValidTags(testTags, regex); /* * Then */ - expect(mockListTags).toHaveBeenCalled(); expect(validTags).toHaveLength(1); }); @@ -114,19 +110,15 @@ describe('utils', () => { node_id: 'string', }, ]; - const mockListTags = jest - .spyOn(github, 'listTags') - .mockImplementation(async () => testTags); /* * When */ - const validTags = await getValidTags(regex, false); + const validTags = await getValidTags(testTags, regex); /* * Then */ - expect(mockListTags).toHaveBeenCalled(); expect(validTags[0]).toEqual({ name: 'v1.2.4-prerelease.2', commit: { sha: 'string', url: 'string' }, @@ -163,17 +155,13 @@ describe('utils', () => { node_id: 'string', }, ]; - const mockListTags = jest - .spyOn(github, 'listTags') - .mockImplementation(async () => testTags); /* * When */ - const validTags = await getValidTags(/^app1\//, false); + const validTags = await getValidTags(testTags, /^app1\//); /* * Then */ - expect(mockListTags).toHaveBeenCalled(); expect(validTags).toHaveLength(1); expect(validTags[0]).toEqual({ name: 'app1/3.0.0',