Skip to content

Commit

Permalink
feat: use gh release to calculate next version (#15)
Browse files Browse the repository at this point in the history
* initial poc

* feat: use release for version calculation

* update index test

* clean up method interface

* update readme and input types

* fix readme typo

* fix run tests

* add dist build

* fix release references
  • Loading branch information
jveldboom authored Feb 17, 2023
1 parent 3ba83a9 commit 8a73acd
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 87 deletions.
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,17 @@ Design Descisions
# Default version bump (major, minor or patch)
# Used when unable to calculate the bump from the commit messages
# For example when not using conventional commits
# Default: minor
# Default: patch
default-bump: ''

# Ignore prereleases when calculating the next version
# Default: false
ignore-drafts: ''

# Ignore draft releases when calculating the next version
# Default: false
ignore-prereleases: ''

# Set the versioning mode to run (future use-case)
# Default: default
mode: ''
Expand All @@ -37,6 +45,8 @@ Design Descisions

## Example Use-Cases
### Auto version on any push to the `main` branch
This example will create a new GitHub release on any push to main as well as update the floating major version `v1`

```yaml
---
name: release
Expand Down Expand Up @@ -94,10 +104,12 @@ yarn lint
- [x] Release v1 of action
- [x] Workflow to run regresssion tests with compiled action
- [x] List action in marketplace
- [x] Improve index.js file
- Should it be simplified and wrapped in a try/catch?
- How can we get 100% test coverage on it?
- [ ] Add version suffix that are semver
- [ ] Improve integration testing to cover all use-case. May require the ability to pass in a list of commits
- [ ] Improve run.js file
- Should it be simplified and wrapped in a try/catch?
- [ ] Output version bump (major, minor, patch) No specific use case but I believe it will be useful

## Notes
- Commit Analyzer https://github.com/semantic-release/commit-analyzer#releaserules
Expand Down
9 changes: 6 additions & 3 deletions action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ branding:
inputs:
github-token:
description: GitHub token
required: false
default: ${{ github.token }}
default-bump:
description: Default version bump (major, minor, or patch)
required: false
default: patch
ignore-prereleases:
description: Ignore prereleases when calculating the next version
default: false
ignore-drafts:
description: Ignore draft releases when calculating the next version
default: false
mode:
description: Sets the version mode to run - future use-case
required: false
default: default

outputs:
Expand Down
43 changes: 31 additions & 12 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44095,13 +44095,25 @@ const getOctokit = (token) => {
return github.getOctokit(token)
}

const getLatestTag = async (octokit, owner, repo) => {
const res = await octokit.request('GET /repos/{owner}/{repo}/tags?per_page=1', {
const getLatestRelease = async ({ octokit, owner, repo, ignoreDrafts = false, ignorePrereleases = false }) => {
const res = await octokit.request('GET /repos/{owner}/{repo}/releases', {
owner,
repo
})

if (res?.data?.length >= 1) return res.data[0]
if (!Array.isArray(res?.data) || res?.data?.length < 1) return
return filterAndSortReleases({ releases: res.data, ignoreDrafts, ignorePrereleases })
}

const filterAndSortReleases = ({ releases = [], ignoreDrafts = false, ignorePrereleases = false }) => {
// apply filters to releases
if (ignoreDrafts) releases = releases.filter(r => r.draft !== true)
if (ignorePrereleases) releases = releases.filter(r => r.prerelease !== true)

// return early if all releases were filtered out
if (releases.length === 0) return

return releases[0]
}

const compareCommits = async (octokit, owner, repo, base, head) => {
Expand Down Expand Up @@ -44130,7 +44142,8 @@ const createRelease = async (octokit, owner, repo, tag) => {

module.exports = {
getOctokit,
getLatestTag,
getLatestRelease,
filterAndSortReleases,
compareCommits,
createRelease
}
Expand All @@ -44152,28 +44165,34 @@ module.exports = async () => {
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
const sha = process.env.GITHUB_SHA

let latestTag
let latestRelease
try {
latestTag = await github.getLatestTag(octokit, owner, repo)
latestRelease = await github.getLatestRelease({
octokit,
owner,
repo,
ignoreDrafts: core.getBooleanInput('ignore-drafts'),
ignorePrereleases: core.getBooleanInput('ignore-prereleases')
})
} catch (err) {
return core.setFailed(`unable to get latest tag - error: ${err.message} ${err?.response?.status}`)
return core.setFailed(`unable to get latest release - error: ${err.message} ${err?.response?.status}`)
}

// return a default version if no previous github tags
if (!latestTag) {
if (!latestRelease) {
const incrementedVersion = semver.inc('0.0.0', core.getInput('default-bump'))
return utils.setVersionOutputs(incrementedVersion)
}

if (!semver.valid(latestTag.name)) {
return core.setFailed(`latest tag name is not valid semver: ${JSON.stringify(latestTag)}`)
if (!semver.valid(latestRelease.name)) {
return core.setFailed(`latest tag name is not valid semver: ${JSON.stringify(latestRelease)}`)
}

// get commits from last tag and calculate version bump
const commits = await github.compareCommits(octokit, owner, repo, latestTag?.commit?.sha, sha)
const commits = await github.compareCommits(octokit, owner, repo, latestRelease.target_commitish, sha)
const bump = await utils.getVersionBump(commits, core.getInput('default-bump'))

const incrementedVersion = semver.inc(latestTag.name, bump)
const incrementedVersion = semver.inc(latestRelease.name, bump)
utils.setVersionOutputs(incrementedVersion)
}

Expand Down
21 changes: 17 additions & 4 deletions src/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@ const getOctokit = (token) => {
return github.getOctokit(token)
}

const getLatestTag = async (octokit, owner, repo) => {
const res = await octokit.request('GET /repos/{owner}/{repo}/tags?per_page=1', {
const getLatestRelease = async ({ octokit, owner, repo, ignoreDrafts = false, ignorePrereleases = false }) => {
const res = await octokit.request('GET /repos/{owner}/{repo}/releases', {
owner,
repo
})

if (res?.data?.length >= 1) return res.data[0]
if (!Array.isArray(res?.data) || res?.data?.length < 1) return
return filterAndSortReleases({ releases: res.data, ignoreDrafts, ignorePrereleases })
}

const filterAndSortReleases = ({ releases = [], ignoreDrafts = false, ignorePrereleases = false }) => {
// apply filters to releases
if (ignoreDrafts) releases = releases.filter(r => r.draft !== true)
if (ignorePrereleases) releases = releases.filter(r => r.prerelease !== true)

// return early if all releases were filtered out
if (releases.length === 0) return

return releases[0]
}

const compareCommits = async (octokit, owner, repo, base, head) => {
Expand Down Expand Up @@ -39,7 +51,8 @@ const createRelease = async (octokit, owner, repo, tag) => {

module.exports = {
getOctokit,
getLatestTag,
getLatestRelease,
filterAndSortReleases,
compareCommits,
createRelease
}
107 changes: 58 additions & 49 deletions src/github.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,73 +14,82 @@ jest.mock('@actions/github', () => {
})

describe('github', () => {
afterEach(() => { jest.clearAllMocks() })

describe('getOctokit', () => {
it('wraps @actions/github\'s getOctokit', () => {
expect(github.getOctokit()).toBe(mockOctokit)
})
})

describe('getLatestTag', () => {
let tag

afterEach(() => {
tag = undefined
describe('getLatestRelease()', () => {
it('should return undefined when no releases', async () => {
const rel = await github.getLatestRelease({ octokit: mockOctokit, owner: 'owner', repo: 'repo' })
expect(rel).toBe(undefined)
})

describe('when the targeted repo has existing tags', () => {
beforeEach(async () => {
mockOctokit.request.mockImplementation(() => {
return {
data: ['1.0.0']
}
})

tag = await github.getLatestTag(mockOctokit, 'owner', 'repo')
})

it('fetches the repo\'s tags via the GitHub API', () => {
expect(mockOctokit.request).toHaveBeenCalledWith('GET /repos/{owner}/{repo}/tags?per_page=1', {
owner: 'owner',
repo: 'repo'
})
it('should return a single release', async () => {
mockOctokit.request.mockImplementation(() => {
return {
data: [
{ name: 'v1.0' },
{ name: 'v1.2' },
{ name: 'v1.3' }
]
}
})

it('returns the latest tag returned by the GitHub API', () => {
expect(tag).toBe('1.0.0')
})
const rel = await github.getLatestRelease({ octokit: mockOctokit, owner: 'owner', repo: 'repo' })
expect(rel).toStrictEqual({ name: 'v1.0' })
})
})

describe('when the targeted repo has no existing tags', () => {
beforeEach(async () => {
mockOctokit.request.mockImplementation(() => {
return {
data: []
}
})

tag = await github.getLatestTag(mockOctokit, 'owner', 'repo')
})
describe('filterAndSortReleases()', () => {
it('should return undefined when no releases', () => {
const res = github.filterAndSortReleases({})
expect(res).toBe(undefined)
})

it('fetches the repo\'s tags via the GitHub API', () => {
expect(mockOctokit.request).toHaveBeenCalledWith('GET /repos/{owner}/{repo}/tags?per_page=1', {
owner: 'owner',
repo: 'repo'
})
})
it('should filter out draft releases and return single release', () => {
const releases = [
{ name: 'v1.0.0', draft: true, prerelease: false },
{ name: 'v1.1.0', draft: false, prerelease: false },
{ name: 'v1.2.0', draft: false, prerelease: false }
]
const res = github.filterAndSortReleases({ releases, ignoreDrafts: true })
expect(res).toStrictEqual(releases[1])
})

it('returns undefined', () => {
expect(tag).toBe(undefined)
})
it('should filter out prereleases and return single release', () => {
const releases = [
{ name: 'v1.0.0', draft: false, prerelease: true },
{ name: 'v1.1.0', draft: false, prerelease: true },
{ name: 'v1.2.0', draft: false, prerelease: false }
]
const res = github.filterAndSortReleases({ releases, ignorePrereleases: true })
expect(res).toStrictEqual(releases[2])
})

describe('when the underlying octokit API request fails', () => {
it('throws an error', async () => {
const result = 'error'
it('should filter out draft & prereleases and return single release', () => {
const releases = [
{ name: 'v1.0.0', draft: true, prerelease: false },
{ name: 'v1.1.0', draft: false, prerelease: true },
{ name: 'v1.2.0', draft: false, prerelease: false },
{ name: 'v1.3.0', draft: true, prerelease: true }

mockOctokit.request.mockRejectedValueOnce(result)
]
const res = github.filterAndSortReleases({ releases, ignoreDrafts: true, ignorePrereleases: true })
expect(res).toStrictEqual(releases[2])
})

await expect(github.getLatestTag(mockOctokit, 'owner', 'repo')).rejects.toMatch(result)
})
it('should filter out all releases and return undefined', () => {
const releases = [
{ name: 'v1.0.0', draft: true, prerelease: false },
{ name: 'v1.1.0', draft: false, prerelease: true },
{ name: 'v1.3.0', draft: true, prerelease: true }
]
const res = github.filterAndSortReleases({ releases, ignoreDrafts: true, ignorePrereleases: true })
expect(res).toStrictEqual(undefined)
})
})

Expand Down
8 changes: 5 additions & 3 deletions src/local.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
process.env['INPUT_GITHUB-TOKEN'] = process.env.GITHUB_TOKEN
process.env.GITHUB_REPOSITORY = 'jveldboom/version-testing'
process.env.GITHUB_SHA = '5f2b80818f3ec006216a7dd4311168c3f1020071'
process.env['INPUT_VERSION-PREFIX'] = 'v'
process.env.GITHUB_REPOSITORY = 'jveldboom/action-conventional-versioning'
process.env.GITHUB_SHA = '9979f8482f38936b74c942a7210dd1caf771eafe'
process.env['INPUT_DEFAULT-BUMP'] = 'minor'
process.env['INPUT_IGNORE-DRAFTS'] = false
process.env['INPUT_IGNORE-PRERELEASES'] = false
require('./index')
22 changes: 14 additions & 8 deletions src/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,33 @@ module.exports = async () => {
const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/')
const sha = process.env.GITHUB_SHA

let latestTag
let latestRelease
try {
latestTag = await github.getLatestTag(octokit, owner, repo)
latestRelease = await github.getLatestRelease({
octokit,
owner,
repo,
ignoreDrafts: core.getBooleanInput('ignore-drafts'),
ignorePrereleases: core.getBooleanInput('ignore-prereleases')
})
} catch (err) {
return core.setFailed(`unable to get latest tag - error: ${err.message} ${err?.response?.status}`)
return core.setFailed(`unable to get latest release - error: ${err.message} ${err?.response?.status}`)
}

// return a default version if no previous github tags
if (!latestTag) {
if (!latestRelease) {
const incrementedVersion = semver.inc('0.0.0', core.getInput('default-bump'))
return utils.setVersionOutputs(incrementedVersion)
}

if (!semver.valid(latestTag.name)) {
return core.setFailed(`latest tag name is not valid semver: ${JSON.stringify(latestTag)}`)
if (!semver.valid(latestRelease.name)) {
return core.setFailed(`latest tag name is not valid semver: ${JSON.stringify(latestRelease)}`)
}

// get commits from last tag and calculate version bump
const commits = await github.compareCommits(octokit, owner, repo, latestTag?.commit?.sha, sha)
const commits = await github.compareCommits(octokit, owner, repo, latestRelease.target_commitish, sha)
const bump = await utils.getVersionBump(commits, core.getInput('default-bump'))

const incrementedVersion = semver.inc(latestTag.name, bump)
const incrementedVersion = semver.inc(latestRelease.name, bump)
utils.setVersionOutputs(incrementedVersion)
}
Loading

0 comments on commit 8a73acd

Please sign in to comment.