Skip to content

Commit

Permalink
Merge pull request #32 from issue-ops/version-check
Browse files Browse the repository at this point in the history
Version check
  • Loading branch information
ncalteen authored Nov 7, 2023
2 parents 98f1db1 + bcd8757 commit 4eb15e5
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 8 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ jobs:
| `manifest-path` | The path to the manifest file that contains the version. |
| | Relative to the root of the repository. |
| | If not set, `use-version` must be set. |
| `overwrite` | Set to `'true'` for the action to overwrite existing tags. |
| | Default: `'false'` |
| `ref` | The Git ref to tag with the specified or inferred version. |
| | Defaults to the base ref of a pull request event trigger. |
| `use-version` | The version you want to explicitly use. |
Expand All @@ -111,6 +113,13 @@ The action outputs the following (assuming the version in the manifest file is
| `patch` | The patch version | `3` |
| `prerelease` | The prerelease version | `alpha.4` |

## Errors

If the `overwrite` parameter is `'false'` (the default value), this action will
fail if there is an existing version tag in the repository that matches the
inferred or provided version. This is to prevent releases from overwriting one
another.

## Example

Assume a Node.js repository has the following tag and commit structure:
Expand Down
52 changes: 50 additions & 2 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jest.mock('@actions/core', () => {
debug: jest.fn(),
getInput: jest.fn(),
info: jest.fn(),
error: jest.fn(),
setFailed: jest.fn(),
setOutput: jest.fn()
}
Expand Down Expand Up @@ -71,6 +72,8 @@ describe('main', () => {
return '.'
case 'manifest-path':
return '/fixtures/valid/node/package.json'
case 'overwrite':
return 'true'
default:
return ''
}
Expand All @@ -83,7 +86,8 @@ describe('main', () => {
patch: '3',
prerelease: 'alpha.4',
toString: jest.fn().mockReturnValue('1.2.3-alpha.4'),
tag: jest.fn()
tag: jest.fn(),
exists: jest.fn().mockImplementation(() => false)
} as Version

jest.spyOn(Version, 'infer').mockReturnValue(mockVersion)
Expand All @@ -104,6 +108,8 @@ describe('main', () => {
return '.'
case 'manifest-path':
return '/fixtures/valid/node/package.json'
case 'overwrite':
return 'true'
default:
return ''
}
Expand All @@ -129,6 +135,8 @@ describe('main', () => {
return '.'
case 'manifest-path':
return '/fixtures/valid/node/package.json'
case 'overwrite':
return 'true'
default:
return ''
}
Expand All @@ -141,7 +149,8 @@ describe('main', () => {
patch: '3',
prerelease: 'alpha.4',
toString: jest.fn().mockReturnValue('1.2.3-alpha.4'),
tag: jest.fn()
tag: jest.fn(),
exists: jest.fn().mockImplementation(() => false)
} as Version

jest.spyOn(Version, 'infer').mockReturnValue(mockVersion)
Expand Down Expand Up @@ -170,6 +179,8 @@ describe('main', () => {
return 'refs/heads/main'
case 'use-version':
return '1.2.3-alpha.4'
case 'overwrite':
return 'true'
default:
return ''
}
Expand All @@ -187,4 +198,41 @@ describe('main', () => {
expect(setOutputMock).toHaveBeenCalledWith('prerelease', 'alpha.4')
expect(runMock).toHaveReturned()
})

it('fails if the version exists', async () => {
// Return valid values for all inputs
jest.spyOn(core, 'getInput').mockImplementation((name: string): string => {
switch (name) {
case 'ref':
return 'refs/heads/main'
case 'workspace':
return '.'
case 'manifest-path':
return '/fixtures/valid/node/package.json'
case 'overwrite':
return 'false'
default:
return ''
}
})

// Mock the return value from Version.infer()
const mockVersion = {
major: '1',
minor: '2',
patch: '3',
prerelease: 'alpha.4',
toString: jest.fn().mockReturnValue('1.2.3-alpha.4'),
tag: jest.fn(),
exists: jest.fn().mockImplementation(() => true)
} as Version

jest.spyOn(Version, 'infer').mockReturnValue(mockVersion)

await main.run()

expect(runMock).toHaveBeenCalled()
expect(setFailedMock).toHaveBeenCalled()
expect(runMock).toHaveReturned()
})
})
43 changes: 43 additions & 0 deletions __tests__/version.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ describe('version', () => {
jest.clearAllMocks()
})

it('removes a leading "v"', async () => {
expect(new Version('v1.2.3-alpha.4').major).toEqual('1')
expect(new Version('V1.2.3-alpha.4').major).toEqual('1')
})

it('fails if an invalid version string was passed to the constructor', async () => {
try {
// Pass an invalid version
Expand Down Expand Up @@ -530,4 +535,42 @@ describe('version', () => {
expect.any(Object)
)
})

it('exists returns true if the tag exists', async () => {
// Mock the exec package to return the tag
const execMock = exec as jest.MockedFunction<typeof exec>
execMock.mockImplementation(
(
commandLine: string,
args?: string[],
options?: any
): Promise<number> => {
options.listeners.stdout(Buffer.from('v1.2.3-alpha.4'))
return Promise.resolve(0)
}
)

const version = new Version('1.2.3-alpha.4')

expect(await version.exists(`${__dirname}/fixtures/valid`)).toBe(true)
})

it('exists returns false if the tag does not exist', async () => {
// Mock the exec package to return the tag
const execMock = exec as jest.MockedFunction<typeof exec>
execMock.mockImplementation(
(
commandLine: string,
args?: string[],
options?: any
): Promise<number> => {
options.listeners.stdout(Buffer.from(''))
return Promise.resolve(0)
}
)

const version = new Version('1.2.3-alpha.4')

expect(await version.exists(`${__dirname}/fixtures/valid`)).toBe(false)
})
})
7 changes: 6 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ branding:

inputs:
manifest-path:
default: package.json
description:
The path to the manifest file that contains the version information,
relative to the root of the repository. If not set, then `version` should
be set to an explicit version in SemVer 2.0 format.
required: false
overwrite:
default: 'false'
description:
If tags already exist for the specified or inferred version, setting this
to 'true' will overwrite them. Defaults to 'false'.
required: false
ref:
default: ${{ github.base_ref }}
description:
Expand Down
29 changes: 29 additions & 0 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.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "semver",
"description": "Semantically version GitHub repository tags",
"version": "0.1.2",
"version": "0.2.0",
"author": "Nick Alteen <[email protected]>",
"homepage": "https://github.com/issue-ops/semver#readme",
"repository": {
Expand Down
8 changes: 7 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Version } from './version'

export async function run() {
const manifestPath: string = core.getInput('manifest-path')
const overwrite: boolean = core.getInput('overwrite') === 'true'
const ref: string = core.getInput('ref')
const useVersion: string = core.getInput('use-version')
const workspace: string = core.getInput('workspace')
Expand Down Expand Up @@ -31,12 +32,17 @@ export async function run() {
return
}

// Check if the tags already exist in the repository.
if (!overwrite && (await version.exists(workspace))) {
core.setFailed("Version already exists and 'overwrite' is false")
return
}

core.info(`Inferred Version: ${version.toString()}`)
core.info(`Tagging ${ref} with version ${version.toString()}`)

// Tag and push the version in the workspace
await version.tag(ref, workspace)

core.info('Tagging complete')

// Output the various version formats
Expand Down
27 changes: 27 additions & 0 deletions src/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export class Version {
constructor(version: string) {
core.info(`Input version string: ${version}`)

if (version.startsWith('v') || version.startsWith('V'))
version = version.slice(1)

// Build metadata is separated by a `+`
// https://semver.org/#spec-item-10
// TODO: Would we eventually want to support this?
Expand Down Expand Up @@ -231,4 +234,28 @@ export class Version {
if (tagOptions.stderr.includes('[new tag]') === false)
throw new Error(tagOptions.stderr)
}

/**
* Checks if the version tags already exist in the repository
*
* @param manifestPath The path to the manifest file
* @param workspace The project workspace
* @returns True if the version tags exist, otherwise false
*/
async exists(workspace: string): Promise<boolean> {
// There's no need to check for anything other than the "full" tag (with the
// prerelease, if present). The major.minor or major tags may exist and can
// be moved.
core.info(`Checking for tag: v${this.toString(true)}`)

const tagOptions: TagOptions = new TagOptions(workspace)

await exec(`git tag -l "${this.toString(true)}"`, [], tagOptions.options)

core.debug(`STDOUT: ${tagOptions.stdout}`)
core.debug(`STDERR: ${tagOptions.stderr}`)

if (tagOptions.stdout.includes(this.toString(true))) return true
else return false
}
}

0 comments on commit 4eb15e5

Please sign in to comment.