diff --git a/src/index.ts b/src/index.ts index e0bf305..8f6946e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ import { install } from './util/install' import debugLib from 'debug' import { forceRelease, initialRelease } from './plugin' import { PluginSpec } from './semantic-release' +import { updateTags } from './util/updateTags' +import { gitConfig } from './util/gitConfig' export default async function run(env = process.env): Promise { try { @@ -73,11 +75,27 @@ export default async function run(env = process.env): Promise { core.setOutput('version', nextRelease.version) core.setOutput('gitTag', nextRelease.gitTag) - const parts = ['major', 'minor', 'patch', 'revision'] - const v = nextRelease.version.split(/\D/, 4) - parts.forEach((k, i) => core.setOutput(k, v[i])) + const versionRegExp = /^(?\d+)\.(?\d+)\.(?\d+)(?:[-](?(?:(?\w+)\.)?\d+))?/ + const version = nextRelease.version.match(versionRegExp)?.groups as { + major: string, + minor: string, + patch: string, + revision: string | undefined, + revisionType: string | undefined + } + Object.entries(version).forEach(([k, v]) => core.setOutput(k, v ?? '')) core.setOutput('notes', nextRelease.notes) + + if (!dryRun) { + const [tagPrefix] = nextRelease.gitTag.split(nextRelease.version) + await gitConfig(env) + const updatedTags = await updateTags('HEAD', nextRelease.version, version, tagPrefix) + if (updatedTags.length) { + core.info(`Updated tags: ${updatedTags.join(', ')}`) + } + } + } catch(e) { core.setFailed(e?.message ?? e) } diff --git a/src/util/gitConfig.ts b/src/util/gitConfig.ts new file mode 100644 index 0000000..4d34ea2 --- /dev/null +++ b/src/util/gitConfig.ts @@ -0,0 +1,12 @@ +import { spawn } from './spawn' + +export async function gitConfig(env: {[k: string]: string | undefined}): Promise { + const name = env.GITHUB_ACTOR || 'github-actions[bot]' + + const email = env.GITHUB_ACTOR + ? `${env.GITHUB_ACTOR}@users.noreply.github.com` + : '41898282+github-actions[bot]@users.noreply.github.com' + + await spawn('git', ['config', '--global', 'user.name', name]) + await spawn('git', ['config', '--global', 'user.email', email]) +} diff --git a/src/util/install.ts b/src/util/install.ts index 8efdce6..3f8d30f 100644 --- a/src/util/install.ts +++ b/src/util/install.ts @@ -2,7 +2,7 @@ import { info } from '@actions/core' import { spawn } from './spawn' import { resolve } from './resolve' -export function install(packages: string[], log: (msg: string) => void): Promise { +export function install(packages: string[], log: (msg: string) => void): Promise { const missing = packages.filter(resolvableName => { try { const module = resolve(resolvableName) @@ -25,5 +25,5 @@ export function install(packages: string[], log: (msg: string) => void): Promise return spawn('npm', args, {}) } - return Promise.resolve() + return Promise.resolve('') } diff --git a/src/util/spawn.ts b/src/util/spawn.ts index b9bc221..630aea3 100644 --- a/src/util/spawn.ts +++ b/src/util/spawn.ts @@ -1,12 +1,16 @@ import * as core from '@actions/core' import * as child_process from 'child_process' -export function spawn(cmd: string, args: string[] = [], options: child_process.SpawnOptions = {}): Promise { - return new Promise((res, rej) => { +export function spawn(cmd: string, args: string[] = [], options: child_process.SpawnOptions = {}): Promise { + return new Promise((res, rej) => { const child = child_process.spawn(cmd, args, options) + let output = '' const buffer = {out: '', err: ''} function addBuffered(type: keyof typeof buffer, data: Buffer) { + if (type === 'out') { + output += data + } buffer[type] += data sendBuffered(type) } @@ -33,7 +37,7 @@ export function spawn(cmd: string, args: string[] = [], options: child_process.S sendBuffered('out', true) sendBuffered('err', true) if (code === 0) { - res() + res(output) } else { rej(`${cmd} ${JSON.stringify(args)} failed: ${signal ?? code}`) } diff --git a/src/util/updateTags.ts b/src/util/updateTags.ts new file mode 100644 index 0000000..7d65561 --- /dev/null +++ b/src/util/updateTags.ts @@ -0,0 +1,42 @@ +import { spawn } from './spawn' + +export async function updateTags( + ref: string, + versionString: string, + version: { + major: string, + minor: string, + patch: string, + revision: string | undefined, + revisionType: string | undefined, + }, + tagPrefix = '', +): Promise { + const tags: string[] = [] + + if (!version.revision) { + const minorTag = `${tagPrefix}${version.major}.${version.minor}` + tags.push(minorTag) + + const nextMinor = `${tagPrefix}${version.major}.${Number(version.minor) + 1}.0` + const hasNextMinor = await spawn('git', ['ls-remote', 'origin', `refs/tags/${nextMinor}`]) + if (!hasNextMinor) { + const majorTag = `${tagPrefix}${version.major}` + tags.push(majorTag) + } + } else if (version.revisionType) { + const revisionTag = `${tagPrefix}${version.major}.${version.minor}.${version.patch}-${version.revisionType}` + tags.push(revisionTag) + } + + if (tags.length) { + for (const tag of tags) { + await spawn('git', ['tag', '-fam', versionString, tag, ref]) + } + await spawn('git', ['push', '-f', 'origin', + ...tags.map(t => `refs/tags/${t}:refs/tags/${t}`), + ]) + } + + return tags +} diff --git a/test/_releaseResult.ts b/test/_releaseResult.ts index 482113d..4adf41c 100644 --- a/test/_releaseResult.ts +++ b/test/_releaseResult.ts @@ -68,4 +68,4 @@ export default { }, ], // typing does not match documented output -} as unknown as SemanticRelease.Result +} as unknown as Extract diff --git a/test/index.ts b/test/index.ts index 65623f7..b3b4ce6 100644 --- a/test/index.ts +++ b/test/index.ts @@ -2,6 +2,7 @@ import type SemanticRelease from 'semantic-release' import run from '../src/index' import { forceRelease, initialRelease } from '../src/plugin' import defaultResult from './_releaseResult' +import type { updateTags as realUpdateTags } from '../src/util/updateTags' const releaseResult = defaultResult as SemanticRelease.Result @@ -39,6 +40,20 @@ jest.mock('../src/util/install', () => ({ install: (packages: string[]) => install(packages), })) +let gitConfig: () => Promise +jest.mock('../src/util/gitConfig', () => ({ + gitConfig: () => gitConfig(), +})) + +let updateTags: typeof realUpdateTags +jest.mock('../src/util/updateTags', () => ({ + updateTags: (...a: Parameters) => updateTags(...a), +})) + +jest.mock('../src/util/spawn', () => ({ + spawn: () => { throw 'this should not have been called - every helper should be mocked' }, +})) + function setup() { const exec = (input = {}, releaseResult: SemanticRelease.Result = false, env = {}) => { setFailed = jest.fn() @@ -50,6 +65,8 @@ function setup() { install = jest.fn(() => Promise.resolve('')) release = jest.fn(() => releaseResult) + gitConfig = jest.fn(() => Promise.resolve('')) + updateTags = jest.fn(() => Promise.resolve([])) return run(env) } @@ -105,7 +122,8 @@ it('output release informations', () => { major: '1', minor: '1', patch: '0', - revision: undefined, + revision: '', + revisionType: '', notes: 'Release notes for version 1.1.0...', }) }) @@ -180,3 +198,26 @@ it('run with inline config', () => { }) }) }) + +it('call updateTag', () => { + const { exec } = setup() + + const run = exec(undefined, {...defaultResult, nextRelease: { + gitHead: 'abc', + gitTag: 'v1.2.3-foo.1', + notes: 'some notes...', + type: 'major', + version: '1.2.3-foo.1', + }}) + + return run.finally(() => { + expect(gitConfig).toBeCalled() + expect(updateTags).toBeCalledWith('HEAD', '1.2.3-foo.1', { + major: '1', + minor: '2', + patch: '3', + revision: 'foo.1', + revisionType: 'foo', + }, 'v') + }) +}) diff --git a/test/util/gitConfig.ts b/test/util/gitConfig.ts new file mode 100644 index 0000000..9d6d6ff --- /dev/null +++ b/test/util/gitConfig.ts @@ -0,0 +1,16 @@ +import { gitConfig } from '../../src/util/gitConfig' + +let spawnMock: (...a: unknown[]) => Promise +jest.mock('../../src/util/spawn', () => ({ + spawn: (...a: unknown[]) => spawnMock(...a), +})) + +test('set github actor', async () => { + spawnMock = jest.fn() + + await gitConfig({GITHUB_ACTOR: 'foo'}) + + expect(spawnMock).toHaveBeenNthCalledWith(1, 'git', ['config', '--global', 'user.name', 'foo']) + expect(spawnMock).toHaveBeenNthCalledWith(2, 'git', ['config', '--global', 'user.email', 'foo@users.noreply.github.com']) + expect(spawnMock).toHaveBeenCalledTimes(2) +}) diff --git a/test/util/install.ts b/test/util/install.ts index 4d171ae..bcfed6a 100644 --- a/test/util/install.ts +++ b/test/util/install.ts @@ -5,7 +5,7 @@ jest.mock('../../src/util/resolve', () => ({ resolve: (name: string) => resolveMock(name), })) -let spawnMock: (...a: unknown[]) => Promise +let spawnMock: (...a: unknown[]) => Promise jest.mock('../../src/util/spawn', () => ({ spawn: (...a: unknown[]) => spawnMock(...a), })) diff --git a/test/util/spawn.ts b/test/util/spawn.ts index 6177f21..ad6bc15 100644 --- a/test/util/spawn.ts +++ b/test/util/spawn.ts @@ -23,7 +23,7 @@ test('defer args and options', () => { expect(spawnMock).toBeCalledWith('foo', ['bar', 'baz'], {uid: 123456}) - return expect(child).resolves.toBe(undefined) + return expect(child).resolves.toBe('') }) test('reject on spawn error', () => { @@ -58,6 +58,15 @@ test('defer stdout to debug', () => { }) }) +test('resolve to stdout', () => { + coreDebug = [] + spawnMock = jest.fn(() => realSpawn(process.execPath, ['-e', `process.stdout.write('some output')`])) + + const child = spawn('foo', ['bar', 'baz'], { uid: 123456 }) + + return expect(child).resolves.toBe('some output') +}) + test('defer stderr to warning', () => { coreWarning = [] spawnMock = jest.fn(() => realSpawn(process.execPath, ['-e', `process.stderr.write('foo')`])) diff --git a/test/util/updateTags.ts b/test/util/updateTags.ts new file mode 100644 index 0000000..058db22 --- /dev/null +++ b/test/util/updateTags.ts @@ -0,0 +1,41 @@ +import { updateTags } from '../../src/util/updateTags' + +let spawnMock: (...a: unknown[]) => Promise +jest.mock('../../src/util/spawn', () => ({ + spawn: (...a: unknown[]) => spawnMock(...a), +})) + +test('set tags', async () => { + spawnMock = jest.fn() + + await updateTags('abc', '1.2.3', {major: '1', minor: '2', patch: '3', revision: undefined, revisionType: undefined}, 'version') + + expect(spawnMock).toHaveBeenNthCalledWith(1, 'git', ['ls-remote', 'origin', 'refs/tags/version1.3.0']) + expect(spawnMock).toHaveBeenNthCalledWith(2, 'git', ['tag', '-fam', '1.2.3', 'version1.2', 'abc']) + expect(spawnMock).toHaveBeenNthCalledWith(3, 'git', ['tag', '-fam', '1.2.3', 'version1', 'abc']) + expect(spawnMock).toHaveBeenNthCalledWith(4, 'git', ['push', '-f', 'origin', 'refs/tags/version1.2:refs/tags/version1.2', 'refs/tags/version1:refs/tags/version1']) + expect(spawnMock).toHaveBeenCalledTimes(4) +}) + +test('omit major on maintenance release', async () => { + spawnMock = jest.fn().mockReturnValueOnce(Promise.resolve('abcdef\trefs/tags/v1.3.0')) + + await updateTags('abc', '1.2.3', {major: '1', minor: '2', patch: '3', revision: undefined, revisionType: undefined}, 'v') + + expect(spawnMock).toHaveBeenNthCalledWith(1, 'git', ['ls-remote', 'origin', 'refs/tags/v1.3.0']) + expect(spawnMock).toHaveBeenNthCalledWith(2, 'git', ['tag', '-fam', '1.2.3', 'v1.2', 'abc']) + expect(spawnMock).toHaveBeenNthCalledWith(3, 'git', ['push', '-f', 'origin', 'refs/tags/v1.2:refs/tags/v1.2']) + expect(spawnMock).toHaveBeenCalledTimes(3) +}) + +test('set tag for prerelease', async () => { + spawnMock = jest.fn().mockReturnValueOnce(Promise.resolve('abcdef\trefs/tags/v1.3.0')) + + await updateTags('abc', '1.2.3-foo.1', {major: '1', minor: '2', patch: '3', revision: 'foo.1', revisionType: 'foo'}, 'v') + + expect(spawnMock).toHaveBeenNthCalledWith(1, 'git', ['tag', '-fam', '1.2.3-foo.1', 'v1.2.3-foo', 'abc']) + expect(spawnMock).toHaveBeenNthCalledWith(2, 'git', ['push', '-f', 'origin', 'refs/tags/v1.2.3-foo:refs/tags/v1.2.3-foo']) + expect(spawnMock).toHaveBeenCalledTimes(2) +}) + +