Rewrite/refactor for v5, migrate to TypeScript (MAJOR)

Set Jest config file in package script

Include module path

Include tests in project folders

Remove index module exports

Hardcode configuration parameters

Move parameter binding into main run function

Use alias imports

Run test sequentially

Remove cleanup (async conflict)

Revert Jest option

Increase test timeout to 15 seconds
This commit is contained in:
Paul Hatcherian 2022-04-03 17:49:40 -04:00
parent dc8f0c5755
commit 31f4e3fdf0
74 changed files with 9422 additions and 4342 deletions

View file

@ -20,7 +20,7 @@ inputs:
description: "A string which, if present in a git commit, indicates that a change represents a minor (feature) change. Wrap with '/' to match using a regular expression."
required: true
default: "(MINOR)"
format:
version_format:
description: "Pattern to use when formatting output version"
required: true
default: "${major}.${minor}.${patch}"
@ -30,14 +30,18 @@ inputs:
namespace:
description: "Use to create a named sub-version. This value will be appended to tags created for this version."
required: false
short_tags:
description: "If false, only full versions, i.e. 'v1.0.0', will be supported as tags. If true, tags will support truncated minor and patch versions such as 'v1' (default)."
required: true
default: "true"
bump_each_commit:
description: "If true, every commit will be treated as a bump to the version."
required: true
default: "false"
search_commit_body:
description: "If true, the body of commits will also be searched for major/minor patterns to determine the version type."
required: true
default: "false"
user_format_type:
description: "The output method used to generate list of users, 'csv' or 'json'. Default is 'csv'."
required: true
default: "csv"
outputs:
major:
description: "Current major number"
@ -54,5 +58,5 @@ outputs:
changed:
description: "Indicates whether there was a change since the last version if change_path was specified. If no change_path was specified this value will always be true since the entire repo is considered."
runs:
using: "node12"
using: "node16"
main: "dist/index.js"

4274
dist/index.js vendored

File diff suppressed because it is too large Load diff

1
dist/index.js.map vendored Normal file

File diff suppressed because one or more lines are too long

84
dist/licenses.txt vendored Normal file
View file

@ -0,0 +1,84 @@
@actions/core
MIT
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@actions/exec
MIT
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@actions/http-client
MIT
Actions Http Client for Node.js
Copyright (c) GitHub, Inc.
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@actions/io
MIT
The MIT License (MIT)
Copyright 2019 GitHub
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
tunnel
MIT
The MIT License (MIT)
Copyright (c) 2012 Koichi Kobayashi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1
dist/sourcemap-register.js vendored Normal file

File diff suppressed because one or more lines are too long

260
index.js
View file

@ -1,260 +0,0 @@
const core = require('@actions/core');
const exec = require("@actions/exec");
const eol = '\n';
const tagPrefix = core.getInput('tag_prefix') || '';
const namespace = core.getInput('namespace') || '';
const shortTags = core.getInput('short_tags') === 'true';
const bumpEachCommit = core.getInput('bump_each_commit') === 'true';
const cmd = async (command, ...args) => {
let output = '', errors = '';
const options = {
silent: true
};
options.listeners = {
stdout: (data) => { output += data.toString(); },
stderr: (data) => { errors += data.toString(); },
ignoreReturnCode: true,
silent: true
};
await exec.exec(command, args, options)
.catch(err => { core.info(`The command '${command} ${args.join(' ')}' failed: ${err}`); });
if (errors !== '') {
core.info(`stderr: ${errors}`);
}
return output;
};
const setOutput = (major, minor, patch, increment, changed, branch, namespace) => {
const format = core.getInput('format', { required: true });
var version = format
.replace('${major}', major)
.replace('${minor}', minor)
.replace('${patch}', patch)
.replace('${increment}', increment);
if (namespace !== '') {
version += `-${namespace}`
}
let tag;
if (!shortTags || major === 0 || patch !== 0) {
// Always tag pre-release/major version 0 as full version
tag = `${tagPrefix}${major}.${minor}.${patch}`;
} else if (minor !== 0) {
tag = `${tagPrefix}${major}.${minor}`;
} else {
tag = `${tagPrefix}${major}`;
}
if (namespace !== '') {
tag += `-${namespace}`
}
const repository = process.env.GITHUB_REPOSITORY;
if (!changed) {
core.info('No changes detected for this commit');
}
core.info(`Version is ${version}`);
if (repository !== undefined && !namespace) {
core.info(`To create a release for this version, go to https://github.com/${repository}/releases/new?tag=${tag}&target=${branch.split('/').reverse()[0]}`);
}
core.setOutput("version", version);
core.setOutput("major", major.toString());
core.setOutput("minor", minor.toString());
core.setOutput("patch", patch.toString());
core.setOutput("increment", increment.toString());
core.setOutput("changed", changed.toString());
core.setOutput("version_tag", tag);
};
const parseVersion = (tag) => {
let stripedTag;
if (tagPrefix.includes('/') && tag.includes(tagPrefix)) {
let tagParts = tag
.replace(tagPrefix, '<--!PREFIX!-->')
.split('/');
stripedTag = tagParts[tagParts.length - 1]
.replace('<--!PREFIX!-->', tagPrefix);
} else {
let tagParts = tag.split('/');
stripedTag = tagParts[tagParts.length - 1];
}
let versionValues = stripedTag
.substr(tagPrefix.length)
.slice(0, namespace === '' ? 999 : -(namespace.length + 1))
.split('.');
let major = parseInt(versionValues[0]);
let minor = versionValues.length > 1 ? parseInt(versionValues[1]) : 0;
let patch = versionValues.length > 2 ? parseInt(versionValues[2]) : 0;
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
throw `Invalid tag ${tag} (${versionValues})`;
}
return [major, minor, patch];
};
const createMatchTest = (pattern) => {
if (pattern.startsWith('/') && pattern.endsWith('/')) {
var regex = new RegExp(pattern.slice(1, -1));
return (l) => regex.test(l);
} else {
return (l) => l.includes(pattern);
}
};
async function run() {
try {
let branch = core.getInput('branch', { required: true });
const majorPattern = createMatchTest(core.getInput('major_pattern', { required: true }));
const minorPattern = createMatchTest(core.getInput('minor_pattern', { required: true }));
const changePath = core.getInput('change_path') || '';
if (branch === 'HEAD') {
branch = (await cmd('git', 'rev-parse', 'HEAD')).trim();
}
const versionPattern = shortTags ? '*[0-9.]' : '*[0-9].*[0-9].*[0-9]'
const releasePattern = namespace === '' ? `${tagPrefix}${versionPattern}` : `${tagPrefix}${versionPattern}-${namespace}`;
let major = 0, minor = 0, patch = 0, increment = 0;
let changed = true;
let lastCommitAll = (await cmd('git', 'rev-list', '-n1', '--all')).trim();
if (lastCommitAll === '') {
// empty repo
setOutput('0', '0', '0', '0', changed, branch, namespace);
return;
}
let currentTag = (await cmd(
`git tag --points-at ${branch} ${releasePattern}`
)).trim();
let tag = '';
try {
tag = (await cmd(
'git',
`describe`,
`--tags`,
`--abbrev=0`,
`--match=${releasePattern}`,
`${branch}~1`
)).trim();
}
catch (err) {
tag = '';
}
let root;
if (tag === '') {
if (await cmd('git', 'remote') !== '') {
core.warning('No tags are present for this repository. If this is unexpected, check to ensure that tags have been pulled from the remote.');
}
// no release tags yet, use the initial commit as the root
root = '';
} else {
// parse the version tag
[major, minor, patch] = parseVersion(tag);
root = await cmd('git', `merge-base`, tag, branch);
}
root = root.trim();
var logCommand = `git log --pretty="%s" --author-date-order ${(root === '' ? branch : `${root}..${branch}`)}`;
if (changePath !== '') {
logCommand += ` -- ${changePath}`;
}
const log = await cmd(logCommand);
if (changePath !== '') {
if (root === '') {
const changedFiles = await cmd(`git log --name-only --oneline ${branch} -- ${changePath}`);
changed = changedFiles.length > 0;
} else {
const changedFiles = await cmd(`git diff --name-only ${root}..${branch} -- ${changePath}`);
changed = changedFiles.length > 0;
}
}
let history = log
.trim()
.split(eol)
.reverse();
if (bumpEachCommit) {
core.info(history)
history.forEach(line => {
if (currentTag) {
[major, minor, patch] = parseVersion(currentTag);
} else if (majorPattern(line)) {
major += 1;
minor = 0;
patch = 0;
} else if (minorPattern(line)) {
minor += 1;
patch = 0;
} else {
patch += 1;
}
});
setOutput(major, minor, patch, increment, changed, branch, namespace);
return;
}
// Discover the change time from the history log by finding the oldest log
// that could set the version.
const majorIndex = history.findIndex(x => majorPattern(x));
const minorIndex = history.findIndex(x => minorPattern(x));
if (majorIndex !== -1) {
increment = history.length - (majorIndex + 1);
patch = 0;
minor = 0;
major++;
} else if (minorIndex !== -1) {
increment = history.length - (minorIndex + 1);
patch = 0;
minor++;
} else {
increment = history.length - 1;
patch++;
}
if (currentTag) {
let tagVersion = parseVersion(currentTag);
if (tagVersion[0] !== major ||
tagVersion[1] !== minor ||
tagVersion[2] !== patch) {
[major, minor, patch] = tagVersion;
increment = 0;
}
}
setOutput(major, minor, patch, increment, changed, branch, namespace);
} catch (error) {
core.error(error);
core.setFailed(error.message);
}
}
run();

View file

@ -1,580 +0,0 @@
const cp = require('child_process');
const path = require('path');
const process = require('process');
const os = require('os');
const windows = process.platform === "win32";
// Action input variables
const defaultInputs = {
branch: "HEAD",
tag_prefix: "v",
major_pattern: "(MAJOR)",
minor_pattern: "(MINOR)",
format: "${major}.${minor}.${patch}+${increment}",
short_tags: true,
bump_each_commit: false
};
// Creates a randomly named git repository and returns a function to execute commands in it
const createTestRepo = (inputs) => {
const repoDirectory = path.join(os.tmpdir(), `test${Math.random().toString(36).substring(2, 15)}`);
cp.execSync(`mkdir ${repoDirectory}`);
cp.execSync(`git init ${repoDirectory}`);
const run = (command, extraInputs) => {
const allInputs = Object.assign({ ...defaultInputs }, inputs, extraInputs);
let env = {};
for (let key in allInputs) {
env[`INPUT_${key.toUpperCase()}`] = allInputs[key];
}
return execute(repoDirectory, command, env);
}
// Configure up git user
run(`git config user.name "Test User"`);
run(`git config user.email "test@example.com"`);
let i = 1;
return {
clean: () => execute(os.tmpdir(), windows ? `rmdir /s /q ${repoDirectory}` : `rm -rf ${repoDirectory}`),
makeCommit: (msg, path) => {
if (windows) {
run(`fsutil file createnew ${path !== undefined ? path.trim('/') + '/' : ''}test${i++} 0`);
} else {
run(`touch ${path !== undefined ? path.trim('/') + '/' : ''}test${i++}`);
}
run(`git add --all`);
run(`git commit -m "${msg}"`);
},
runAction: (inputs) => run(`node ${path.join(__dirname, 'index.js')}`, inputs),
exec: run
};
};
// Executes a set of commands in the specified directory
const execute = (workingDirectory, command, env) => {
try {
return String(cp.execSync(command, { env: { ...process.env, ...env }, cwd: workingDirectory }));
}
catch (e) {
console.error(String(e.stdout));
console.error(String(e.stderr));
throw e;
}
};
test('Empty repository version is correct', () => {
const repo = createTestRepo(); // 0.0.0+0
var result = repo.runAction();
expect(result).toMatch('Version is 0.0.0+0');
repo.clean();
});
test('Repository with commits shows increment', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit(`Second Commit`); // 0.0.1+1
const result = repo.runAction();
expect(result).toMatch('Version is 0.0.1+1');
repo.clean();
});
test('Repository show commit for checked out commit', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit(`Second Commit`); // 0.0.1+1
let result = repo.runAction();
expect(result).toMatch('Version is 0.0.1+1');
repo.exec(`git checkout HEAD~1`); // 0.0.1+1
result = repo.runAction();
expect(result).toMatch('Version is 0.0.1+0');
repo.clean();
});
test('Tagging does not break version', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit(`Second Commit`); // 0.0.1+1
repo.makeCommit(`Third Commit`); // 0.0.1+2
repo.exec('git tag v0.0.1')
const result = repo.runAction();
expect(result).toMatch('Version is 0.0.1+2');
repo.clean();
});
test('Minor update bumps minor version and resets increment', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MINOR)'); // 0.1.0+0
const result = repo.runAction();
expect(result).toMatch('Version is 0.1.0+0');
repo.clean();
});
test('Major update bumps major version and resets increment', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MAJOR)'); // 1.0.0+0
const result = repo.runAction();
expect(result).toMatch('Version is 1.0.0+0');
repo.clean();
});
test('Multiple major commits are idempotent', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MAJOR)'); // 1.0.0+0
repo.makeCommit('Third Commit (MAJOR)'); // 1.0.0+1
const result = repo.runAction();
expect(result).toMatch('Version is 1.0.0+1');
repo.clean();
});
test('Minor commits after a major commit are ignored', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MAJOR)'); // 1.0.0+0
repo.makeCommit('Third Commit (MINOR)'); // 1.0.0+1
const result = repo.runAction();
expect(result).toMatch('Version is 1.0.0+1');
repo.clean();
});
test('Tags start new version', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.exec('git tag v0.0.1');
repo.makeCommit('Third Commit'); // 0.0.2+0
const result = repo.runAction();
expect(result).toMatch('Version is 0.0.2+0');
repo.clean();
});
test('Version pulled from last release branch', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.exec('git tag v0.0.1');
repo.makeCommit('Second Commit'); // 0.0.2+0
repo.exec('git tag v5.6.7');
repo.makeCommit('Third Commit'); // 5.6.7+0
const result = repo.runAction();
expect(result).toMatch('Version is 5.6.8+0');
repo.clean();
});
/* Removed for now
test('Tags on branches are used', () => {
// This test checks that tags are counted correctly even if they are not on
// the main branch:
// master o--o--o--o <- expecting v0.0.2
// \
// release o--o <- taged v0.0.1
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.makeCommit('Third Commit'); // 0.1.1+2
repo.exec('git checkout -b release/0.0.1')
repo.makeCommit('Fourth Commit'); // 0.1.1+3
repo.exec('git tag v0.0.1');
repo.exec('git checkout master');
repo.makeCommit('Fifth Commit'); // 0.0.2.0
const result = repo.runAction();
expect(result).toMatch('Version is 0.0.2+0');
repo.clean();
});
*/
test('Merged tags do not affect version', () => {
// This test checks that merges don't override tags
// Tagged v0.0.2
// v
// master o--o--o---o---o <- expecting v0.0.3+1
// \ /
// release o---o <- taged v0.0.1
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.makeCommit('Third Commit'); // 0.1.1+2
repo.exec('git checkout -b release/0.0.1')
repo.makeCommit('Fourth Commit'); // 0.1.1+3
repo.exec('git tag v0.0.1');
repo.exec('git checkout master');
repo.makeCommit('Fifth Commit'); // 0.0.2.0
repo.exec('git tag v0.0.2');
repo.exec('git merge release/0.0.1');
const result = repo.runAction();
expect(result).toMatch('Version is 0.0.3+1');
repo.clean();
});
test('Version tags do not require all three version numbers', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit (MAJOR)'); // 1.0.0+0
repo.exec('git tag v1');
repo.makeCommit(`Second Commit`); // 1.0.1+0
const result = repo.runAction();
expect(result).toMatch('Version is 1.0.1+0');
repo.clean();
});
test('Format input is respected', () => {
const repo = createTestRepo({ format: 'M${major}m${minor}p${patch}i${increment}' }); // M0m0p0i0
repo.makeCommit('Initial Commit'); // M1m2p3i0
repo.exec('git tag v1.2.3');
repo.makeCommit(`Second Commit`); // M1m2p4i0
const result = repo.runAction();
expect(result).toMatch('M1m2p4i0');
repo.clean();
});
test('Version prefixes are not required/can be empty', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.makeCommit(`Second Commit`); // 0.0.2
const result = repo.runAction();
expect(result).toMatch('Version is 0.0.2');
repo.clean();
});
test('Tag order comes from commit order, not tag create order', () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.makeCommit('Third Commit'); // 0.0.1+2
repo.exec('git tag v2.0.0');
// Can't timeout in this context on Windows, ping localhost to delay
repo.exec(windows ? 'ping 127.0.0.1 -n 2' : 'sleep 2');
repo.exec('git tag v1.0.0 HEAD~1');
repo.makeCommit('Fourth Commit'); // 0.0.1+2
const result = repo.runAction();
expect(result).toMatch('Version is 2.0.1+0');
repo.clean();
});
test('Change detection is true by default', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.makeCommit(`Second Commit`); // 0.0.2
const result = repo.runAction();
expect(result).toMatch('::set-output name=changed::true');
repo.clean();
});
test('Changes to monitored path is true when change is in path', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.exec('mkdir project1');
repo.makeCommit(`Second Commit`, 'project1'); // 0.0.2
const result = repo.runAction({ change_path: "project1" });
expect(result).toMatch('::set-output name=changed::true');
repo.clean();
});
test('Changes to monitored path is false when changes are not in path', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.exec('mkdir project1');
repo.exec('mkdir project2');
repo.makeCommit(`Second Commit`, 'project2'); // 0.0.2
const result = repo.runAction({ change_path: "project1" });
expect(result).toMatch('::set-output name=changed::false');
repo.clean();
});
test('Changes can be detected without tags', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('mkdir project1');
repo.makeCommit(`Second Commit`, 'project1'); // 0.0.2
const result = repo.runAction({ change_path: "project1" });
expect(result).toMatch('::set-output name=changed::true');
repo.clean();
});
test('Changes to multiple monitored path is true when change is in path', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.exec('mkdir project1');
repo.exec('mkdir project2');
repo.makeCommit(`Second Commit`, 'project2'); // 0.0.2
const result = repo.runAction({ change_path: "project1 project2" });
expect(result).toMatch('::set-output name=changed::true');
repo.clean();
});
test('Changes to multiple monitored path is false when change is not in path', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.exec('mkdir project1');
repo.exec('mkdir project2');
repo.exec('mkdir project3');
repo.makeCommit(`Second Commit`, 'project3'); // 0.0.2
const result = repo.runAction({ change_path: "project1 project2" });
expect(result).toMatch('::set-output name=changed::false');
repo.clean();
});
test('Namespace is tracked separately', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.makeCommit('Second Commit'); // 0.0.2
repo.exec('git tag 0.1.0-subproject');
repo.makeCommit('Third Commit'); // 0.0.2 / 0.1.1
const result = repo.runAction();
const subprojectResult = repo.runAction({ namespace: "subproject" });
expect(result).toMatch('Version is 0.0.2+1');
expect(subprojectResult).toMatch('Version is 0.1.1+0');
repo.clean();
});
test('Commits outside of path are not counted', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit');
repo.makeCommit('Second Commit');
repo.makeCommit('Third Commit');
const result = repo.runAction({ change_path: "project1" });
expect(result).toMatch('Version is 0.0.1+0');
repo.clean();
});
test('Commits inside path are counted', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit');
repo.makeCommit('Second Commit');
repo.makeCommit('Third Commit');
repo.exec('mkdir project1');
repo.makeCommit('Fourth Commit', 'project1'); // 0.0.1+0
repo.makeCommit('Fifth Commit', 'project1'); // 0.0.1+1
repo.makeCommit('Sixth Commit', 'project1'); // 0.0.1+2
const result = repo.runAction({ change_path: "project1" });
expect(result).toMatch('Version is 0.0.1+2');
repo.clean();
});
test('Current tag is used', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit');
repo.makeCommit('Second Commit');
repo.makeCommit('Third Commit');
repo.exec('git tag 7.6.5');
const result = repo.runAction();
expect(result).toMatch('Version is 7.6.5+0');
repo.clean();
});
test('Short tag can be switched off', () => {
const repo = createTestRepo({ tag_prefix: '', short_tags: 'false' }); // 0.0.0
repo.makeCommit('Initial Commit');
repo.makeCommit('Second Commit');
repo.makeCommit('Third Commit');
repo.exec('git tag 7');
const result = repo.runAction();
expect(result).toMatch('Version is 0.0.1+2');
repo.clean();
});
test('Bump each commit works', () => {
const repo = createTestRepo({ tag_prefix: '', bump_each_commit: true }); // 0.0.0
expect(repo.runAction()).toMatch('Version is 0.0.0+0');
repo.makeCommit('Initial Commit');
expect(repo.runAction()).toMatch('Version is 0.0.1+0');
repo.makeCommit('Second Commit');
expect(repo.runAction()).toMatch('Version is 0.0.2+0');
repo.makeCommit('Third Commit');
expect(repo.runAction()).toMatch('Version is 0.0.3+0');
repo.makeCommit('Fourth Commit (MINOR)');
expect(repo.runAction()).toMatch('Version is 0.1.0+0');
repo.makeCommit('Fifth Commit');
expect(repo.runAction()).toMatch('Version is 0.1.1+0');
repo.makeCommit('Sixth Commit (MAJOR)');
expect(repo.runAction()).toMatch('Version is 1.0.0+0');
repo.makeCommit('Seventh Commit');
expect(repo.runAction()).toMatch('Version is 1.0.1+0');
repo.clean();
});
test('Bump each commit picks up tags', () => {
const repo = createTestRepo({ tag_prefix: '', bump_each_commit: true }); // 0.0.0
expect(repo.runAction()).toMatch('Version is 0.0.0+0');
repo.makeCommit('Initial Commit');
expect(repo.runAction()).toMatch('Version is 0.0.1+0');
repo.makeCommit('Second Commit');
expect(repo.runAction()).toMatch('Version is 0.0.2+0');
repo.makeCommit('Third Commit');
repo.exec('git tag 3.0.0');
expect(repo.runAction()).toMatch('Version is 3.0.0+0');
repo.makeCommit('Fourth Commit');
expect(repo.runAction()).toMatch('Version is 3.0.1+0');
repo.clean();
});
test('Increment not affected by matching tag', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.1
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.exec('git tag 0.0.1');
expect(repo.runAction()).toMatch('Version is 0.0.1+1');
repo.clean();
});
test('Regular expressions can be used as major tag', () => {
const repo = createTestRepo({ tag_prefix: '', major_pattern: '/S[a-z]+Value/' }); // 0.0.1
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit SomeValue'); // 0.0.1+1
expect(repo.runAction()).toMatch('Version is 1.0.0+0');
repo.clean();
});
test('Regular expressions can be used as minor tag', () => {
const repo = createTestRepo({ tag_prefix: '', minor_pattern: '/S[a-z]+Value/' }); // 0.0.1
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit SomeValue'); // 0.0.1+1
expect(repo.runAction()).toMatch('Version is 0.1.0+0');
repo.clean();
});
test('Short tags disabled matches full tags', () => {
const repo = createTestRepo({ short_tags: 'false' }); // 0.0.0
repo.makeCommit('Initial Commit');
repo.makeCommit('Second Commit');
repo.makeCommit('Third Commit');
repo.exec('git tag v1.2.3');
const result = repo.runAction();
expect(result).toMatch('Version is 1.2.3+0');
repo.clean();
});
test('Tag prefix can include forward slash', () => {
const repo = createTestRepo({ short_tags: 'false', tag_prefix: 'version/' }); // 0.0.0
repo.makeCommit('Initial Commit');
repo.exec('git tag version/1.2.3');
const result = repo.runAction();
expect(result).toMatch('Version is 1.2.3+0');
repo.clean();
});

12
jest.config.js Normal file
View file

@ -0,0 +1,12 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true,
modulePaths: [
"<rootDir>/src/"
],
}

29
lib/ActionConfig.js Normal file
View file

@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActionConfig = void 0;
/** Represents the input configuration for the semantic-version action */
class ActionConfig {
constructor() {
/** Set to specify a specific branch, default is the current HEAD */
this.branch = "HEAD";
/** The prefix to use to identify tags */
this.tagPrefix = "v";
/** A string which, if present in a git commit, indicates that a change represents a major (breaking) change. Wrap with '/' to match using a regular expression. */
this.majorPattern = "(MAJOR)";
/** A string which, if present in a git commit, indicates that a change represents a minor (feature) change. Wrap with '/' to match using a regular expression. */
this.minorPattern = "(MINOR)";
/** Pattern to use when formatting output version */
this.versionFormat = '${major}.${minor}.${patch}';
/** Path to check for changes. If any changes are detected in the path the 'changed' output will true. Enter multiple paths separated by spaces. */
this.changePath = '';
/** Use to create a named sub-version. This value will be appended to tags created for this version. */
this.namespace = "";
/** If true, every commit will be treated as a bump to the version. */
this.bumpEachCommit = false;
/** If true, the body of commits will also be searched for major/minor patterns to determine the version type */
this.searchCommitBody = false;
/** The output method used to generate list of users, 'csv' or 'json'. Default is 'csv'. */
this.userFormatType = "csv";
}
}
exports.ActionConfig = ActionConfig;

60
lib/CommandRunner.js Normal file
View file

@ -0,0 +1,60 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.cmd = void 0;
// Using require instead of import to support integration testing
const exec = __importStar(require("@actions/exec"));
const cmd = (command, ...args) => __awaiter(void 0, void 0, void 0, function* () {
let output = '', errors = '';
const options = {
silent: true,
listeners: {
stdout: (data) => { output += data.toString(); },
stderr: (data) => { errors += data.toString(); },
ignoreReturnCode: true,
silent: true
}
};
try {
yield exec.exec(command, args, options);
}
catch (err) {
//core.info(`The command cd '${command} ${args.join(' ')}' failed: ${err}`);
}
if (errors !== '') {
//core.info(`stderr: ${errors}`);
}
return output;
});
exports.cmd = cmd;

View file

@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigurationProvider = void 0;
const CsvUserFormatter_1 = require("./formatting/CsvUserFormatter");
const DefaultTagFormatter_1 = require("./formatting/DefaultTagFormatter");
const DefaultVersionFormatter_1 = require("./formatting/DefaultVersionFormatter");
const JsonUserFormatter_1 = require("./formatting/JsonUserFormatter");
const DefaultCommitsProvider_1 = require("./providers/DefaultCommitsProvider");
const DefaultCurrentCommitResolver_1 = require("./providers/DefaultCurrentCommitResolver");
const DefaultVersionClassifier_1 = require("./providers/DefaultVersionClassifier");
const TagLastReleaseResolver_1 = require("./providers/TagLastReleaseResolver");
const BumpAlwaysVersionClassifier_1 = require("./providers/BumpAlwaysVersionClassifier");
class ConfigurationProvider {
constructor(config) {
this.config = config;
}
GetCurrentCommitResolver() { return new DefaultCurrentCommitResolver_1.DefaultCurrentCommitResolver(this.config); }
GetLastReleaseResolver() { return new TagLastReleaseResolver_1.TagLastReleaseResolver(this.config); }
GetCommitsProvider() { return new DefaultCommitsProvider_1.DefaultCommitsProvider(this.config); }
GetVersionClassifier() {
if (this.config.bumpEachCommit) {
return new BumpAlwaysVersionClassifier_1.BumpAlwaysVersionClassifier(this.config);
}
return new DefaultVersionClassifier_1.DefaultVersionClassifier(this.config);
}
GetVersionFormatter() { return new DefaultVersionFormatter_1.DefaultVersionFormatter(this.config); }
GetTagFormatter() { return new DefaultTagFormatter_1.DefaultTagFormatter(this.config); }
GetUserFormatter() {
switch (this.config.userFormatType) {
case 'json': return new JsonUserFormatter_1.JsonUserFormatter(this.config);
case 'csv': return new CsvUserFormatter_1.CsvUserFormatter(this.config);
default:
throw new Error(`Unknown user format type: ${this.config.userFormatType}, supported types: json, csv`);
}
}
}
exports.ConfigurationProvider = ConfigurationProvider;

30
lib/VersionResult.js Normal file
View file

@ -0,0 +1,30 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VersionResult = void 0;
/** Represents the total output for the action */
class VersionResult {
/**
* Creates a new result instance
* @param major - The major version number
* @param minor - The minor version number
* @param patch - The patch version number
* @param increment - The number of commits for this version (usually used to create version suffix)
* @param formattedVersion - The formatted semantic version
* @param versionTag - The string to be used as a Git tag
* @param changed - True if the version was changed, otherwise false
* @param authors - Authors formatted according to the format mode (e.g. JSON, CSV, YAML, etc.)
* @param currentCommit - The current commit hash
*/
constructor(major, minor, patch, increment, formattedVersion, versionTag, changed, authors, currentCommit) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.increment = increment;
this.formattedVersion = formattedVersion;
this.versionTag = versionTag;
this.changed = changed;
this.authors = authors;
this.currentCommit = currentCommit;
}
}
exports.VersionResult = VersionResult;

52
lib/action.js Normal file
View file

@ -0,0 +1,52 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runAction = void 0;
const VersionResult_1 = require("./VersionResult");
const VersionType_1 = require("./providers/VersionType");
const UserInfo_1 = require("./providers/UserInfo");
const VersionInformation_1 = require("./providers/VersionInformation");
function runAction(configurationProvider) {
return __awaiter(this, void 0, void 0, function* () {
const currentCommitResolver = configurationProvider.GetCurrentCommitResolver();
const lastReleaseResolver = configurationProvider.GetLastReleaseResolver();
const commitsProvider = configurationProvider.GetCommitsProvider();
const versionClassifier = configurationProvider.GetVersionClassifier();
const versionFormatter = configurationProvider.GetVersionFormatter();
const tagFormmater = configurationProvider.GetTagFormatter();
const userFormatter = configurationProvider.GetUserFormatter();
if (yield currentCommitResolver.IsEmptyRepoAsync()) {
let versionInfo = new VersionInformation_1.VersionInformation(0, 0, 0, 0, VersionType_1.VersionType.None, [], false);
return new VersionResult_1.VersionResult(versionInfo.major, versionInfo.minor, versionInfo.patch, versionInfo.increment, versionFormatter.Format(versionInfo), tagFormmater.Format(versionInfo), versionInfo.changed, userFormatter.Format('author', []), '');
}
const currentCommit = yield currentCommitResolver.ResolveAsync();
const lastRelease = yield lastReleaseResolver.ResolveAsync(currentCommit, tagFormmater);
const commitSet = yield commitsProvider.GetCommitsAsync(lastRelease.hash, currentCommit);
const classification = yield versionClassifier.ClassifyAsync(lastRelease, commitSet);
const { major, minor, patch, increment, type, changed } = classification;
// At this point all necessary data has been pulled from the database, create
// version information to be used by the formatters
let versionInfo = new VersionInformation_1.VersionInformation(major, minor, patch, increment, type, commitSet.commits, changed);
// Group all the authors together, count the number of commits per author
const allAuthors = versionInfo.commits
.reduce((acc, commit) => {
const key = `${commit.author} <${commit.authorEmail}>`;
acc[key] = acc[key] || { n: commit.author, e: commit.authorEmail, c: 0 };
acc[key].c++;
return acc;
}, {});
const authors = Object.values(allAuthors)
.map((u) => new UserInfo_1.UserInfo(u.n, u.e, u.c))
.sort((a, b) => b.commits - a.commits);
return new VersionResult_1.VersionResult(versionInfo.major, versionInfo.minor, versionInfo.patch, versionInfo.increment, versionFormatter.Format(versionInfo), tagFormmater.Format(versionInfo), versionInfo.changed, userFormatter.Format('author', authors), currentCommit);
});
}
exports.runAction = runAction;

View file

@ -0,0 +1,12 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CsvUserFormatter = void 0;
class CsvUserFormatter {
constructor(config) {
// placeholder for consistency with other formatters
}
Format(type, users) {
return users.map(user => `${user.name} <${user.email}>`).join(', ');
}
}
exports.CsvUserFormatter = CsvUserFormatter;

View file

@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultTagFormatter = void 0;
/** Default tag formatter which allows a prefix to be specified */
class DefaultTagFormatter {
constructor(config) {
this.namespace = config.namespace;
this.tagPrefix = config.tagPrefix;
this.namespaceSeperator = '-'; // maybe make configurable in the future
}
Format(versionInfo) {
const result = `${this.tagPrefix}${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`;
if (!!this.namespace) {
return `${result}${this.namespaceSeperator}${this.namespace}`;
}
return result;
}
GetPattern() {
if (!!this.namespace) {
return `${this.tagPrefix}*[0-9].*[0-9].*[0-9]${this.namespaceSeperator}${this.namespace}`;
}
return `${this.tagPrefix}*[0-9].*[0-9].*[0-9]`;
}
Parse(tag) {
let stripedTag;
if (this.tagPrefix.includes('/') && tag.includes(this.tagPrefix)) {
let tagParts = tag
.replace(this.tagPrefix, '<--!PREFIX!-->')
.split('/');
stripedTag = tagParts[tagParts.length - 1]
.replace('<--!PREFIX!-->', this.tagPrefix);
}
else {
let tagParts = tag.split('/');
stripedTag = tagParts[tagParts.length - 1];
}
let versionValues = stripedTag
.substring(this.tagPrefix.length)
.slice(0, this.namespace === '' ? 999 : -(this.namespace.length + 1))
.split('.');
let major = parseInt(versionValues[0]);
let minor = versionValues.length > 1 ? parseInt(versionValues[1]) : 0;
let patch = versionValues.length > 2 ? parseInt(versionValues[2]) : 0;
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
throw `Invalid tag ${tag} (${versionValues})`;
}
return [major, minor, patch];
}
;
}
exports.DefaultTagFormatter = DefaultTagFormatter;

View file

@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultVersionFormatter = void 0;
class DefaultVersionFormatter {
constructor(config) {
this.formatString = config.versionFormat;
}
Format(versionInfo) {
return this.formatString
.replace('${major}', versionInfo.major.toString())
.replace('${minor}', versionInfo.minor.toString())
.replace('${patch}', versionInfo.patch.toString())
.replace('${increment}', versionInfo.increment.toString());
}
}
exports.DefaultVersionFormatter = DefaultVersionFormatter;

View file

@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonUserFormatter = void 0;
class JsonUserFormatter {
constructor(config) {
// placeholder for consistency with other formatters
}
Format(type, users) {
let result = users.map(u => ({ name: u.name, email: u.email }));
return JSON.stringify(result).replace('\n', '');
}
}
exports.JsonUserFormatter = JsonUserFormatter;

View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.YamlUserFormatter = void 0;
class YamlUserFormatter {
constructor(config) {
this.lineBreak = config.userFormatLineBreak || '\n';
this.includeType = config.includeType || false;
}
Format(type, users) {
const result = users.flatMap(u => [`- name: "${u.name}"`, ` email: "${u.email}"`]).join(this.lineBreak);
return this.includeType ?
`${type}:${this.lineBreak}${result}` :
result;
}
}
exports.YamlUserFormatter = YamlUserFormatter;

11
lib/formatting/index.js Normal file
View file

@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsonUserFormatter = exports.DefaultVersionFormatter = exports.DefaultTagFormatter = exports.CsvUserFormatter = void 0;
var CsvUserFormatter_1 = require("./CsvUserFormatter");
Object.defineProperty(exports, "CsvUserFormatter", { enumerable: true, get: function () { return CsvUserFormatter_1.CsvUserFormatter; } });
var DefaultTagFormatter_1 = require("./DefaultTagFormatter");
Object.defineProperty(exports, "DefaultTagFormatter", { enumerable: true, get: function () { return DefaultTagFormatter_1.DefaultTagFormatter; } });
var DefaultVersionFormatter_1 = require("./DefaultVersionFormatter");
Object.defineProperty(exports, "DefaultVersionFormatter", { enumerable: true, get: function () { return DefaultVersionFormatter_1.DefaultVersionFormatter; } });
var JsonUserFormatter_1 = require("./JsonUserFormatter");
Object.defineProperty(exports, "JsonUserFormatter", { enumerable: true, get: function () { return JsonUserFormatter_1.JsonUserFormatter; } });

85
lib/main.js Normal file
View file

@ -0,0 +1,85 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.run = void 0;
const action_1 = require("./action");
const ConfigurationProvider_1 = require("./ConfigurationProvider");
const core = __importStar(require("@actions/core"));
function setOutput(versionResult) {
const { major, minor, patch, increment, formattedVersion, versionTag, changed, authors, currentCommit } = versionResult;
const repository = process.env.GITHUB_REPOSITORY;
if (!changed) {
core.info('No changes detected for this commit');
}
core.info(`Version is ${formattedVersion}`);
if (repository !== undefined) {
core.info(`To create a release for this version, go to https://github.com/${repository}/releases/new?tag=${versionTag}&target=${currentCommit.split('/').slice(-1)[0]}`);
}
core.setOutput("version", formattedVersion);
core.setOutput("major", major.toString());
core.setOutput("minor", minor.toString());
core.setOutput("patch", patch.toString());
core.setOutput("increment", increment.toString());
core.setOutput("changed", changed.toString());
core.setOutput("version_tag", versionTag);
core.setOutput("authors", authors);
}
function run() {
return __awaiter(this, void 0, void 0, function* () {
const config = {
branch: core.getInput('branch'),
tagPrefix: core.getInput('tag_prefix'),
majorPattern: core.getInput('major_pattern'),
minorPattern: core.getInput('minor_pattern'),
versionFormat: core.getInput('version_format'),
changePath: core.getInput('change_path'),
namespace: core.getInput('namespace'),
bumpEachCommit: core.getInput('bump_each_commit') === 'true',
searchCommitBody: core.getInput('search_commit_body') === 'true',
userFormatType: core.getInput('user_format_type')
};
if (config.versionFormat === '' && core.getInput('format') !== '') {
core.warning(`The 'format' input is deprecated, use 'versionFormat' instead`);
config.versionFormat = core.getInput('format');
}
if (core.getInput('short_tags') !== '') {
core.warning(`The 'short_tags' input option is no longer supported`);
}
const configurationProvider = new ConfigurationProvider_1.ConfigurationProvider(config);
const result = yield (0, action_1.runAction)(configurationProvider);
setOutput(result);
});
}
exports.run = run;
run();

View file

@ -0,0 +1,52 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BumpAlwaysVersionClassifier = void 0;
const DefaultVersionClassifier_1 = require("./DefaultVersionClassifier");
const VersionClassification_1 = require("./VersionClassification");
const VersionType_1 = require("./VersionType");
class BumpAlwaysVersionClassifier extends DefaultVersionClassifier_1.DefaultVersionClassifier {
constructor(config) {
super(config);
// Placeholder for consistency
}
ClassifyAsync(lastRelease, commitSet) {
return __awaiter(this, void 0, void 0, function* () {
if (lastRelease.currentPatch !== null) {
return new VersionClassification_1.VersionClassification(VersionType_1.VersionType.None, 0, false, lastRelease.currentMajor, lastRelease.currentMinor, lastRelease.currentPatch);
}
let { major, minor, patch } = lastRelease;
let type = VersionType_1.VersionType.None;
if (commitSet.commits.length === 0) {
return new VersionClassification_1.VersionClassification(type, 0, false, major, minor, patch);
}
for (let commit of commitSet.commits.reverse()) {
if (this.majorPattern(commit)) {
major += 1;
minor = 0;
patch = 0;
type = VersionType_1.VersionType.Major;
}
else if (this.minorPattern(commit)) {
minor += 1;
patch = 0;
type = VersionType_1.VersionType.Minor;
}
else {
patch += 1;
type = VersionType_1.VersionType.Patch;
}
}
return new VersionClassification_1.VersionClassification(type, 0, true, major, minor, patch);
});
}
}
exports.BumpAlwaysVersionClassifier = BumpAlwaysVersionClassifier;

View file

@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommitInfo = void 0;
/** Represents information about a commit */
class CommitInfo {
/**
* Creates a new commit information instance
* @param hash - The hash of the commit
* @param subject - The subject of the commit message
* @param body - The body of the commit message
* @param author - The author's name
* @param authorEmail - The author's email
* @param authorDate - The date the commit was authored
* @param committer - The committer's name
* @param committerEmail - The committer's email
* @param committerDate - The date the commit was committed
* @param tags - List of any tags associated with this commit
*/
constructor(hash, subject, body, author, authorEmail, authorDate, committer, committerEmail, committerDate, tags) {
this.hash = hash;
this.subject = subject;
this.body = body;
this.author = author;
this.authorEmail = authorEmail;
this.authorDate = authorDate;
this.committer = committer;
this.committerEmail = committerEmail;
this.committerDate = committerDate;
this.tags = tags;
}
}
exports.CommitInfo = CommitInfo;

View file

@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CommitInfoSet = void 0;
/** Represents information about a set of commits */
class CommitInfoSet {
constructor(changed, commits) {
this.changed = changed;
this.commits = commits;
}
}
exports.CommitInfoSet = CommitInfoSet;

View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,79 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultCommitsProvider = void 0;
const CommandRunner_1 = require("../CommandRunner");
const CommitInfo_1 = require("./CommitInfo");
const CommitInfoSet_1 = require("./CommitInfoSet");
class DefaultCommitsProvider {
constructor(config) {
this.changePath = config.changePath;
}
GetCommitsAsync(startHash, endHash) {
return __awaiter(this, void 0, void 0, function* () {
const logSplitter = `@@@START_RECORD`;
const formatPlaceholders = Object.entries({
hash: '%H',
subject: '%s',
body: '%b',
author: '%an',
authorEmail: '%ae',
authorDate: '%aI',
committer: '%cn',
committerEmail: '%ce',
committerDate: '%cI',
tags: '%d'
});
const pretty = logSplitter + '%n' + formatPlaceholders
.map(x => `@@@${x[0]}%n${x[1]}`)
.join('%n');
var logCommand = `git log --pretty="${pretty}" --author-date-order ${(startHash === '' ? endHash : `${startHash}..${endHash}`)}`;
if (this.changePath !== '') {
logCommand += ` -- ${this.changePath}`;
}
const log = yield (0, CommandRunner_1.cmd)(logCommand);
const entries = log
.split(logSplitter)
.slice(1);
const commits = entries.map(entry => {
const fields = entry
.split(`@@@`)
.slice(1)
.reduce((acc, value) => {
const firstLine = value.indexOf('\n');
const key = value.substring(0, firstLine);
acc[key] = value.substring(firstLine + 1).trim();
return acc;
}, {});
const tags = fields.tags
.split(',')
.map((v) => v.trim())
.filter((v) => v.startsWith('tags: '))
.map((v) => v.substring(5).trim());
return new CommitInfo_1.CommitInfo(fields.hash, fields.subject, fields.body, fields.author, fields.authorEmail, new Date(fields.authorDate), fields.committer, fields.committerEmail, new Date(fields.committerDate), tags);
});
// check for changes
let changed = true;
if (this.changePath !== '') {
if (startHash === '') {
const changedFiles = yield (0, CommandRunner_1.cmd)(`git log --name-only --oneline ${endHash} -- ${this.changePath}`);
changed = changedFiles.length > 0;
}
else {
const changedFiles = yield (0, CommandRunner_1.cmd)(`git diff --name-only ${startHash}..${endHash} -- ${this.changePath}`);
changed = changedFiles.length > 0;
}
}
return new CommitInfoSet_1.CommitInfoSet(changed, commits);
});
}
}
exports.DefaultCommitsProvider = DefaultCommitsProvider;

View file

@ -0,0 +1,33 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultCurrentCommitResolver = void 0;
const CommandRunner_1 = require("../CommandRunner");
class DefaultCurrentCommitResolver {
constructor(config) {
this.branch = config.branch;
}
ResolveAsync() {
return __awaiter(this, void 0, void 0, function* () {
if (this.branch === 'HEAD') {
return (yield (0, CommandRunner_1.cmd)('git', 'rev-parse', 'HEAD')).trim();
}
return this.branch;
});
}
IsEmptyRepoAsync() {
return __awaiter(this, void 0, void 0, function* () {
let lastCommitAll = (yield (0, CommandRunner_1.cmd)('git', 'rev-list', '-n1', '--all')).trim();
return lastCommitAll === '';
});
}
}
exports.DefaultCurrentCommitResolver = DefaultCurrentCommitResolver;

View file

@ -0,0 +1,90 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DefaultVersionClassifier = void 0;
const VersionClassification_1 = require("./VersionClassification");
const VersionType_1 = require("./VersionType");
class DefaultVersionClassifier {
constructor(config) {
const searchBody = config.searchCommitBody;
this.majorPattern = this.parsePattern(config.majorPattern, searchBody);
this.minorPattern = this.parsePattern(config.minorPattern, searchBody);
}
parsePattern(pattern, searchBody) {
if (pattern.startsWith('/') && pattern.endsWith('/')) {
var regex = new RegExp(pattern.slice(1, -1));
return searchBody ?
(commit) => regex.test(commit.subject) || regex.test(commit.body) :
(commit) => regex.test(commit.subject);
}
else {
const matchString = pattern;
return searchBody ?
(commit) => commit.subject.includes(matchString) || commit.body.includes(matchString) :
(commit) => commit.subject.includes(matchString);
}
}
getNextVersion(current, type) {
switch (type) {
case VersionType_1.VersionType.Major:
return { major: current.major + 1, minor: 0, patch: 0 };
case VersionType_1.VersionType.Minor:
return { major: current.major, minor: current.minor + 1, patch: 0 };
case VersionType_1.VersionType.Patch:
return { major: current.major, minor: current.minor, patch: current.patch + 1 };
case VersionType_1.VersionType.None:
return { major: current.major, minor: current.minor, patch: current.patch };
default:
throw new Error(`Unknown change type: ${type}`);
}
}
resolveCommitType(commitsSet) {
if (commitsSet.commits.length === 0) {
return { type: VersionType_1.VersionType.None, increment: 0, changed: commitsSet.changed };
}
const commits = commitsSet.commits.reverse();
let index = 1;
for (let commit of commits) {
if (this.majorPattern(commit)) {
return { type: VersionType_1.VersionType.Major, increment: commits.length - index, changed: commitsSet.changed };
}
index++;
}
index = 1;
for (let commit of commits) {
if (this.minorPattern(commit)) {
return { type: VersionType_1.VersionType.Minor, increment: commits.length - index, changed: commitsSet.changed };
}
index++;
}
return { type: VersionType_1.VersionType.Patch, increment: commitsSet.commits.length - 1, changed: true };
}
ClassifyAsync(lastRelease, commitSet) {
return __awaiter(this, void 0, void 0, function* () {
const { type, increment, changed } = this.resolveCommitType(commitSet);
const { major, minor, patch } = this.getNextVersion(lastRelease, type);
if (lastRelease.currentPatch !== null) {
// If the current commit is tagged, we must use that version. Here we check if the version we have resolved from the
// previous commits is the same as the current version. If it is, we will use the increment value, otherwise we reset
// to zero. For example:
// - commit 1 - v1.0.0+0
// - commit 2 - v1.0.0+1
// - commit 3 was tagged v2.0.0 - v2.0.0+0
// - commit 4 - v2.0.1+0
const versionsMatch = lastRelease.currentMajor === major && lastRelease.currentMinor === minor && lastRelease.currentPatch === patch;
const currentIncremement = versionsMatch ? increment : 0;
return new VersionClassification_1.VersionClassification(VersionType_1.VersionType.None, currentIncremement, false, lastRelease.currentMajor, lastRelease.currentMinor, lastRelease.currentPatch);
}
return new VersionClassification_1.VersionClassification(type, increment, changed, major, minor, patch);
});
}
}
exports.DefaultVersionClassifier = DefaultVersionClassifier;

View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,26 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ReleaseInformation = void 0;
// Finds the hash of the last commit
class ReleaseInformation {
/**
* Creates a new instance
* @param major - the major version number
* @param minor - the minor version number
* @param patch - the patch version number
* @param hash - the hash of commit of the last release
* @param currentMajor - the major version number from the current commit
* @param currentMinor - the minor version number from the current commit
* @param currentPatch - the patch version number from the current commit
*/
constructor(major, minor, patch, hash, currentMajor, currentMinor, currentPatch) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.hash = hash;
this.currentMajor = currentMajor;
this.currentMinor = currentMinor;
this.currentPatch = currentPatch;
}
}
exports.ReleaseInformation = ReleaseInformation;

View file

@ -0,0 +1,72 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TagLastReleaseResolver = void 0;
const CommandRunner_1 = require("../CommandRunner");
const ReleaseInformation_1 = require("./ReleaseInformation");
const core = __importStar(require("@actions/core"));
class TagLastReleaseResolver {
constructor(config) {
this.changePath = config.changePath;
}
ResolveAsync(current, tagFormatter) {
return __awaiter(this, void 0, void 0, function* () {
const releasePattern = tagFormatter.GetPattern();
let currentTag = (yield (0, CommandRunner_1.cmd)(`git tag --points-at ${current} ${releasePattern}`)).trim();
const [currentMajor, currentMinor, currentPatch] = !!currentTag ? tagFormatter.Parse(currentTag) : [null, null, null];
let tag = '';
try {
tag = (yield (0, CommandRunner_1.cmd)('git', `describe`, `--tags`, `--abbrev=0`, `--match=${releasePattern}`, `${current}~1`)).trim();
}
catch (err) {
tag = '';
}
if (tag === '') {
if ((yield (0, CommandRunner_1.cmd)('git', 'remote')) !== '') {
// Since there is no remote, we assume that there are no other tags to pull. In
// practice this isn't likely to happen, but it keeps the test output from being
// polluted with a bunch of warnings.
core.warning('No tags are present for this repository. If this is unexpected, check to ensure that tags have been pulled from the remote.');
}
// no release tags yet, use the initial commit as the root
return new ReleaseInformation_1.ReleaseInformation(0, 0, 0, '', currentMajor, currentMinor, currentPatch);
}
// parse the version tag
const [major, minor, patch] = tagFormatter.Parse(tag);
const root = yield (0, CommandRunner_1.cmd)('git', `merge-base`, tag, current);
return new ReleaseInformation_1.ReleaseInformation(major, minor, patch, root.trim(), currentMajor, currentMinor, currentPatch);
});
}
}
exports.TagLastReleaseResolver = TagLastReleaseResolver;

18
lib/providers/UserInfo.js Normal file
View file

@ -0,0 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserInfo = void 0;
/** Represents information about a user (e.g. committer, author, tagger) */
class UserInfo {
/**
* Creates a new instance
* @param name - User's name
* @param email - User's email
* @param commits - Number of commits in the scope evaluated
*/
constructor(name, email, commits) {
this.name = name;
this.email = email;
this.commits = commits;
}
}
exports.UserInfo = UserInfo;

View file

@ -0,0 +1,24 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VersionClassification = void 0;
/** The result of a version classification */
class VersionClassification {
/**
* Creates a new version classification result instance
* @param type - The type of change the current range represents
* @param increment - The number of commits which have this version, usually zero-based
* @param changed - True if the version has changed, false otherwise
* @param major - The major version number
* @param minor - The minor version number
* @param patch - The patch version number
*/
constructor(type, increment, changed, major, minor, patch) {
this.type = type;
this.increment = increment;
this.changed = changed;
this.major = major;
this.minor = minor;
this.patch = patch;
}
}
exports.VersionClassification = VersionClassification;

View file

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View file

@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VersionInformation = void 0;
/**
* Represents the "resolved" information about a version change, serves
* as the input to formatters that produce the final output
*/
class VersionInformation {
/**
* Creates a new version information instance
* @param major - The major version number
* @param minor - The minor version number
* @param patch - The patch version number
* @param increment - The number of commits for this version
* @param type - The type of change the current range represents
* @param commits - The list of commits for this version
* @param changed - True if the version has changed, false otherwise
*/
constructor(major, minor, patch, increment, type, commits, changed) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.increment = increment;
this.type = type;
this.commits = commits;
this.changed = changed;
}
}
exports.VersionInformation = VersionInformation;

View file

@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VersionType = void 0;
/** Indicates the type of change a particular version change represents */
var VersionType;
(function (VersionType) {
/** Indicates a major version change */
VersionType[VersionType["Major"] = 0] = "Major";
/** Indicates a minor version change */
VersionType[VersionType["Minor"] = 1] = "Minor";
/** Indicates a patch version change */
VersionType[VersionType["Patch"] = 2] = "Patch";
/** Indicates no change--generally this means that the current commit is already tagged with a version */
VersionType[VersionType["None"] = 3] = "None";
})(VersionType = exports.VersionType || (exports.VersionType = {}));

23
lib/providers/index.js Normal file
View file

@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VersionType = exports.VersionInformation = exports.VersionClassification = exports.UserInfo = exports.TagLastReleaseResolver = exports.ReleaseInformation = exports.DefaultVersionClassifier = exports.DefaultCurrentCommitResolver = exports.DefaultCommitsProvider = exports.CommitInfo = void 0;
var CommitInfo_1 = require("./CommitInfo");
Object.defineProperty(exports, "CommitInfo", { enumerable: true, get: function () { return CommitInfo_1.CommitInfo; } });
var DefaultCommitsProvider_1 = require("./DefaultCommitsProvider");
Object.defineProperty(exports, "DefaultCommitsProvider", { enumerable: true, get: function () { return DefaultCommitsProvider_1.DefaultCommitsProvider; } });
var DefaultCurrentCommitResolver_1 = require("./DefaultCurrentCommitResolver");
Object.defineProperty(exports, "DefaultCurrentCommitResolver", { enumerable: true, get: function () { return DefaultCurrentCommitResolver_1.DefaultCurrentCommitResolver; } });
var DefaultVersionClassifier_1 = require("./DefaultVersionClassifier");
Object.defineProperty(exports, "DefaultVersionClassifier", { enumerable: true, get: function () { return DefaultVersionClassifier_1.DefaultVersionClassifier; } });
var ReleaseInformation_1 = require("./ReleaseInformation");
Object.defineProperty(exports, "ReleaseInformation", { enumerable: true, get: function () { return ReleaseInformation_1.ReleaseInformation; } });
var TagLastReleaseResolver_1 = require("./TagLastReleaseResolver");
Object.defineProperty(exports, "TagLastReleaseResolver", { enumerable: true, get: function () { return TagLastReleaseResolver_1.TagLastReleaseResolver; } });
var UserInfo_1 = require("./UserInfo");
Object.defineProperty(exports, "UserInfo", { enumerable: true, get: function () { return UserInfo_1.UserInfo; } });
var VersionClassification_1 = require("./VersionClassification");
Object.defineProperty(exports, "VersionClassification", { enumerable: true, get: function () { return VersionClassification_1.VersionClassification; } });
var VersionInformation_1 = require("./VersionInformation");
Object.defineProperty(exports, "VersionInformation", { enumerable: true, get: function () { return VersionInformation_1.VersionInformation; } });
var VersionType_1 = require("./VersionType");
Object.defineProperty(exports, "VersionType", { enumerable: true, get: function () { return VersionType_1.VersionType; } });

6160
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,16 @@
{
"name": "semantic-version",
"version": "1.0.0",
"version": "0.0.0",
"description": "Semantic Version GitHub Action",
"main": "index.js",
"main": "lib/main.js",
"scripts": {
"lint": "eslint index.js",
"package": "ncc build index.js -o dist",
"test": "eslint index.js && jest"
"build": "tsc",
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"lint": "eslint src/**/*.ts",
"package": "ncc build --source-map --license licenses.txt",
"test": "jest --config ./jest.config.js",
"all": "npm run build && npm run format && npm run lint && npm run package && npm test"
},
"repository": {
"type": "git",
@ -24,13 +28,20 @@
},
"homepage": "https://github.com/paulhatch/semantic-version#readme",
"dependencies": {
"@actions/core": "^1.5.0",
"@actions/exec": "^1.1.0"
"@actions/core": "^1.6.0",
"@actions/exec": "^1.1.1"
},
"devDependencies": {
"@zeit/ncc": "^0.22.3",
"eslint": "^7.32.0",
"eslint-plugin-jest": "^24.4.2",
"jest": "^27.2.1"
"@types/node": "^17.0.23",
"@typescript-eslint/parser": "^5.17.0",
"@vercel/ncc": "^0.33.3",
"eslint": "^8.12.0",
"eslint-plugin-github": "^4.3.6",
"eslint-plugin-jest": "^26.1.3",
"jest": "^27.5.1",
"js-yaml": "^4.1.0",
"prettier": "^2.6.1",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3"
}
}

23
src/ActionConfig.ts Normal file
View file

@ -0,0 +1,23 @@
/** Represents the input configuration for the semantic-version action */
export class ActionConfig {
/** Set to specify a specific branch, default is the current HEAD */
public branch: string = "HEAD";
/** The prefix to use to identify tags */
public tagPrefix: string = "v";
/** A string which, if present in a git commit, indicates that a change represents a major (breaking) change. Wrap with '/' to match using a regular expression. */
public majorPattern: string = "(MAJOR)";
/** A string which, if present in a git commit, indicates that a change represents a minor (feature) change. Wrap with '/' to match using a regular expression. */
public minorPattern: string = "(MINOR)";
/** Pattern to use when formatting output version */
public versionFormat: string = '${major}.${minor}.${patch}';
/** Path to check for changes. If any changes are detected in the path the 'changed' output will true. Enter multiple paths separated by spaces. */
public changePath: string = '';
/** Use to create a named sub-version. This value will be appended to tags created for this version. */
public namespace: string = "";
/** If true, every commit will be treated as a bump to the version. */
public bumpEachCommit: boolean = false;
/** If true, the body of commits will also be searched for major/minor patterns to determine the version type */
public searchCommitBody: boolean = false;
/** The output method used to generate list of users, 'csv' or 'json'. Default is 'csv'. */
public userFormatType: string = "csv";
}

28
src/CommandRunner.ts Normal file
View file

@ -0,0 +1,28 @@
// Using require instead of import to support integration testing
import * as exec from '@actions/exec';
import * as core from '@actions/core';
export const cmd = async (command: string, ...args: any): Promise<string> => {
let output = '', errors = '';
const options = {
silent: true,
listeners: {
stdout: (data: any) => { output += data.toString(); },
stderr: (data: any) => { errors += data.toString(); },
ignoreReturnCode: true,
silent: true
}
};
try {
await exec.exec(command, args, options);
} catch (err) {
//core.info(`The command cd '${command} ${args.join(' ')}' failed: ${err}`);
}
if (errors !== '') {
//core.info(`stderr: ${errors}`);
}
return output;
};

View file

@ -0,0 +1,52 @@
import { CsvUserFormatter } from './formatting/CsvUserFormatter'
import { DefaultTagFormatter } from './formatting/DefaultTagFormatter'
import { DefaultVersionFormatter } from './formatting/DefaultVersionFormatter'
import { JsonUserFormatter } from './formatting/JsonUserFormatter'
import { TagFormatter } from './formatting/TagFormatter'
import { UserFormatter } from './formatting/UserFormatter'
import { VersionFormatter } from './formatting/VersionFormatter'
import { CommitsProvider } from './providers/CommitsProvider'
import { CurrentCommitResolver } from './providers/CurrentCommitResolver'
import { DefaultCommitsProvider } from './providers/DefaultCommitsProvider'
import { DefaultCurrentCommitResolver } from './providers/DefaultCurrentCommitResolver'
import { DefaultVersionClassifier } from './providers/DefaultVersionClassifier'
import { LastReleaseResolver } from './providers/LastReleaseResolver'
import { TagLastReleaseResolver } from './providers/TagLastReleaseResolver'
import { VersionClassifier } from './providers/VersionClassifier'
import { BumpAlwaysVersionClassifier } from './providers/BumpAlwaysVersionClassifier'
import { ActionConfig } from './ActionConfig';
export class ConfigurationProvider {
constructor(config: ActionConfig) {
this.config = config;
}
private config: ActionConfig;
public GetCurrentCommitResolver(): CurrentCommitResolver { return new DefaultCurrentCommitResolver(this.config); }
public GetLastReleaseResolver(): LastReleaseResolver { return new TagLastReleaseResolver(this.config); }
public GetCommitsProvider(): CommitsProvider { return new DefaultCommitsProvider(this.config); }
public GetVersionClassifier(): VersionClassifier {
if (this.config.bumpEachCommit) {
return new BumpAlwaysVersionClassifier(this.config);
}
return new DefaultVersionClassifier(this.config);
}
public GetVersionFormatter(): VersionFormatter { return new DefaultVersionFormatter(this.config); }
public GetTagFormatter(): TagFormatter { return new DefaultTagFormatter(this.config); }
public GetUserFormatter(): UserFormatter {
switch (this.config.userFormatType) {
case 'json': return new JsonUserFormatter(this.config);
case 'csv': return new CsvUserFormatter(this.config);
default:
throw new Error(`Unknown user format type: ${this.config.userFormatType}, supported types: json, csv`);
}
}
}

27
src/VersionResult.ts Normal file
View file

@ -0,0 +1,27 @@
import { UserInfo } from "./providers/UserInfo";
/** Represents the total output for the action */
export class VersionResult {
/**
* Creates a new result instance
* @param major - The major version number
* @param minor - The minor version number
* @param patch - The patch version number
* @param increment - The number of commits for this version (usually used to create version suffix)
* @param formattedVersion - The formatted semantic version
* @param versionTag - The string to be used as a Git tag
* @param changed - True if the version was changed, otherwise false
* @param authors - Authors formatted according to the format mode (e.g. JSON, CSV, YAML, etc.)
* @param currentCommit - The current commit hash
*/
constructor(
public major: number,
public minor: number,
public patch: number,
public increment: number,
public formattedVersion: string,
public versionTag: string,
public changed: boolean,
public authors: string,
public currentCommit: string) { }
}

67
src/action.ts Normal file
View file

@ -0,0 +1,67 @@
import { ConfigurationProvider } from './ConfigurationProvider';
import { VersionResult } from './VersionResult';
import { VersionType } from './providers/VersionType';
import { UserInfo } from './providers/UserInfo';
import { VersionInformation } from './providers/VersionInformation';
export async function runAction(configurationProvider: ConfigurationProvider): Promise<VersionResult> {
const currentCommitResolver = configurationProvider.GetCurrentCommitResolver();
const lastReleaseResolver = configurationProvider.GetLastReleaseResolver();
const commitsProvider = configurationProvider.GetCommitsProvider();
const versionClassifier = configurationProvider.GetVersionClassifier();
const versionFormatter = configurationProvider.GetVersionFormatter();
const tagFormmater = configurationProvider.GetTagFormatter();
const userFormatter = configurationProvider.GetUserFormatter();
if (await currentCommitResolver.IsEmptyRepoAsync()) {
let versionInfo = new VersionInformation(0, 0, 0, 0, VersionType.None, [], false);
return new VersionResult(
versionInfo.major,
versionInfo.minor,
versionInfo.patch,
versionInfo.increment,
versionFormatter.Format(versionInfo),
tagFormmater.Format(versionInfo),
versionInfo.changed,
userFormatter.Format('author', []),
''
);
}
const currentCommit = await currentCommitResolver.ResolveAsync();
const lastRelease = await lastReleaseResolver.ResolveAsync(currentCommit, tagFormmater);
const commitSet = await commitsProvider.GetCommitsAsync(lastRelease.hash, currentCommit);
const classification = await versionClassifier.ClassifyAsync(lastRelease, commitSet);
const { major, minor, patch, increment, type, changed } = classification;
// At this point all necessary data has been pulled from the database, create
// version information to be used by the formatters
let versionInfo = new VersionInformation(major, minor, patch, increment, type, commitSet.commits, changed);
// Group all the authors together, count the number of commits per author
const allAuthors = versionInfo.commits
.reduce((acc: any, commit) => {
const key = `${commit.author} <${commit.authorEmail}>`;
acc[key] = acc[key] || { n: commit.author, e: commit.authorEmail, c: 0 };
acc[key].c++;
return acc;
}, {});
const authors = Object.values(allAuthors)
.map((u: any) => new UserInfo(u.n, u.e, u.c))
.sort((a: UserInfo, b: UserInfo) => b.commits - a.commits);
return new VersionResult(
versionInfo.major,
versionInfo.minor,
versionInfo.patch,
versionInfo.increment,
versionFormatter.Format(versionInfo),
tagFormmater.Format(versionInfo),
versionInfo.changed,
userFormatter.Format('author', authors),
currentCommit
);
}

View file

@ -0,0 +1,14 @@
import { ActionConfig } from '../ActionConfig';
import { UserInfo } from '../providers/UserInfo';
import { UserFormatter } from './UserFormatter';
export class CsvUserFormatter implements UserFormatter {
constructor(config: ActionConfig) {
// placeholder for consistency with other formatters
}
public Format(type: string, users: UserInfo[]): string {
return users.map(user => `${user.name} <${user.email}>`).join(', ');
}
}

View file

@ -0,0 +1,65 @@
import { TagFormatter } from './TagFormatter';
import { VersionInformation } from "../providers/VersionInformation";
import { ActionConfig } from '../ActionConfig';
/** Default tag formatter which allows a prefix to be specified */
export class DefaultTagFormatter implements TagFormatter {
private tagPrefix: string;
private namespace: string;
private namespaceSeperator: string;
constructor(config: ActionConfig) {
this.namespace = config.namespace;
this.tagPrefix = config.tagPrefix;
this.namespaceSeperator = '-'; // maybe make configurable in the future
}
public Format(versionInfo: VersionInformation): string {
const result = `${this.tagPrefix}${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`;
if (!!this.namespace) {
return `${result}${this.namespaceSeperator}${this.namespace}`;
}
return result;
}
public GetPattern(): string {
if (!!this.namespace) {
return `${this.tagPrefix}*[0-9].*[0-9].*[0-9]${this.namespaceSeperator}${this.namespace}`;
}
return `${this.tagPrefix}*[0-9].*[0-9].*[0-9]`;
}
public Parse(tag: string): [major: number, minor: number, patch: number] {
let stripedTag;
if (this.tagPrefix.includes('/') && tag.includes(this.tagPrefix)) {
let tagParts = tag
.replace(this.tagPrefix, '<--!PREFIX!-->')
.split('/');
stripedTag = tagParts[tagParts.length - 1]
.replace('<--!PREFIX!-->', this.tagPrefix);
} else {
let tagParts = tag.split('/');
stripedTag = tagParts[tagParts.length - 1];
}
let versionValues = stripedTag
.substring(this.tagPrefix.length)
.slice(0, this.namespace === '' ? 999 : -(this.namespace.length + 1))
.split('.');
let major = parseInt(versionValues[0]);
let minor = versionValues.length > 1 ? parseInt(versionValues[1]) : 0;
let patch = versionValues.length > 2 ? parseInt(versionValues[2]) : 0;
if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
throw `Invalid tag ${tag} (${versionValues})`;
}
return [major, minor, patch];
};
}

View file

@ -0,0 +1,20 @@
import { VersionFormatter } from './VersionFormatter';
import { VersionInformation } from "../providers/VersionInformation";
import { ActionConfig } from '../ActionConfig';
export class DefaultVersionFormatter implements VersionFormatter {
private formatString: string;
constructor(config: ActionConfig) {
this.formatString = config.versionFormat;
}
public Format(versionInfo: VersionInformation): string {
return this.formatString
.replace('${major}', versionInfo.major.toString())
.replace('${minor}', versionInfo.minor.toString())
.replace('${patch}', versionInfo.patch.toString())
.replace('${increment}', versionInfo.increment.toString());
}
}

View file

@ -0,0 +1,13 @@
import { ActionConfig } from '../ActionConfig';
import { UserInfo } from '../providers/UserInfo';
import { UserFormatter } from './UserFormatter';
export class JsonUserFormatter implements UserFormatter {
constructor(config: ActionConfig) {
// placeholder for consistency with other formatters
}
public Format(type: string, users: UserInfo[]): string {
let result: any = users.map(u => ({ name: u.name, email: u.email }));
return JSON.stringify(result).replace('\n', '');
}
}

View file

@ -0,0 +1,7 @@
import { VersionInformation } from "../providers/VersionInformation";
export interface TagFormatter {
Format(versionInfo: VersionInformation): string;
GetPattern(): string;
Parse(tag: string): [major: number, minor: number, patch: number];
}

View file

@ -0,0 +1,6 @@
import { UserInfo } from "../providers/UserInfo";
/** Formats a list of users as a string (e.g. JSON, YAML, CSV, etc) */
export interface UserFormatter {
Format(type: string, users: UserInfo[]): string;
}

View file

@ -0,0 +1,6 @@
import { VersionInformation } from "../providers/VersionInformation";
// Formatters
export interface VersionFormatter {
Format(versionInfo: VersionInformation): string;
}

466
src/main.test.ts Normal file
View file

@ -0,0 +1,466 @@
import * as process from 'process'
import * as cp from 'child_process'
import * as path from 'path'
import * as os from 'os';
import { expect, test } from '@jest/globals'
import { runAction } from '../src/action';
import { ConfigurationProvider } from './ConfigurationProvider';
import { ActionConfig } from './ActionConfig';
const windows = process.platform === "win32";
// Creates a randomly named git repository and returns a function to execute commands in it
const createTestRepo = (repoDefaultConfig?: Partial<ActionConfig>) => {
const repoDirectory = path.join(os.tmpdir(), `test${Math.random().toString(36).substring(2, 15)}`);
cp.execSync(`mkdir ${repoDirectory}`);
cp.execSync(`git init ${repoDirectory}`);
const run = (command: string) => {
return execute(repoDirectory, command);
}
// Configure up git user
run(`git config user.name "Test User"`);
run(`git config user.email "test@example.com"`);
let i = 1;
return {
makeCommit: (msg: string, path: string = '') => {
if (windows) {
run(`fsutil file createnew ${path !== '' ? path.trim() + '/' : ''}test${i++} 0`);
} else {
run(`touch ${path !== '' ? path.trim() + '/' : ''}test${i++}`);
}
run(`git add --all`);
run(`git commit -m "${msg}"`);
},
runAction: async (inputs?: Partial<ActionConfig>) => {
let config = new ActionConfig();
config = { ...config, ...{ versionFormat: "${major}.${minor}.${patch}+${increment}" }, ...repoDefaultConfig, ...inputs };
process.chdir(repoDirectory);
return await runAction(new ConfigurationProvider(config));
},
exec: run
};
};
// Executes a set of commands in the specified directory
const execute = (workingDirectory: string, command: string, env?: any) => {
try {
return String(cp.execSync(command, { env: { ...process.env, ...env }, cwd: workingDirectory }));
}
catch (e: any) {
throw e;
}
};
test('Empty repository version is correct', async () => {
const repo = createTestRepo(); // 0.0.0+0
var result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.0+0');
}, 15000);
test('Repository with commits shows increment', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit(`Second Commit`); // 0.0.1+1
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.1+1');
}, 15000);
test('Repository show commit for checked out commit', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit(`Second Commit`); // 0.0.1+1
let result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.1+1');
repo.exec(`git checkout HEAD~1`); // 0.0.1+1
result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.1+0');
}, 15000);
test('Tagging does not break version', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit(`Second Commit`); // 0.0.1+1
repo.makeCommit(`Third Commit`); // 0.0.1+2
repo.exec('git tag v0.0.1')
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.1+2');
}, 15000);
test('Minor update bumps minor version and resets increment', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MINOR)'); // 0.1.0+0
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.1.0+0');
}, 15000);
test('Major update bumps major version and resets increment', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MAJOR)'); // 1.0.0+0
const result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.0+0');
}, 15000);
test('Multiple major commits are idempotent', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MAJOR)'); // 1.0.0+0
repo.makeCommit('Third Commit (MAJOR)'); // 1.0.0+1
const result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.0+1');
}, 15000);
test('Minor commits after a major commit are ignored', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit (MAJOR)'); // 1.0.0+0
repo.makeCommit('Third Commit (MINOR)'); // 1.0.0+1
const result = await repo.runAction();
expect(result.formattedVersion).toBe('1.0.0+1');
}, 15000);
test('Tags start new version', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.exec('git tag v0.0.1');
repo.makeCommit('Third Commit'); // 0.0.2+0
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.2+0');
}, 15000);
test('Version pulled from last release branch', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.exec('git tag v0.0.1');
repo.makeCommit('Second Commit'); // 0.0.2+0
repo.exec('git tag v5.6.7');
repo.makeCommit('Third Commit'); // 5.6.7+0
const result = await repo.runAction();
expect(result.formattedVersion).toBe('5.6.8+0');
}, 15000);
/* Removed for now
test('Tags on branches are used', async () => {
// This test checks that tags are counted correctly even if they are not on
// the main branch:
// master o--o--o--o <- expecting v0.0.2
// \
// release o--o <- taged v0.0.1
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.makeCommit('Third Commit'); // 0.1.1+2
repo.exec('git checkout -b release/0.0.1')
repo.makeCommit('Fourth Commit'); // 0.1.1+3
repo.exec('git tag v0.0.1');
repo.exec('git checkout master');
repo.makeCommit('Fifth Commit'); // 0.0.2.0
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.2+0');
});
*/
test('Merged tags do not affect version', async () => {
// This test checks that merges don't override tags
// Tagged v0.0.2
// v
// master o--o--o---o---o <- expecting v0.0.3+1
// \ /
// release o---o <- taged v0.0.1
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.makeCommit('Third Commit'); // 0.1.1+2
repo.exec('git checkout -b release/0.0.1')
repo.makeCommit('Fourth Commit'); // 0.1.1+3
repo.exec('git tag v0.0.1');
repo.exec('git checkout master');
repo.makeCommit('Fifth Commit'); // 0.0.2.0
repo.exec('git tag v0.0.2');
repo.exec('git merge release/0.0.1');
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.3+1');
}, 15000);
test('Format input is respected', async () => {
const repo = createTestRepo({ versionFormat: 'M${major}m${minor}p${patch}i${increment}' }); // M0m0p0i0
repo.makeCommit('Initial Commit'); // M1m2p3i0
repo.exec('git tag v1.2.3');
repo.makeCommit(`Second Commit`); // M1m2p4i0
const result = await repo.runAction();
expect(result.formattedVersion).toBe('M1m2p4i0');
}, 15000);
test('Version prefixes are not required/can be empty', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.makeCommit(`Second Commit`); // 0.0.2
const result = await repo.runAction();
expect(result.formattedVersion).toBe('0.0.2+0');
}, 15000);
test('Tag order comes from commit order, not tag create order', async () => {
const repo = createTestRepo(); // 0.0.0+0
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.makeCommit('Third Commit'); // 0.0.1+2
repo.exec('git tag v2.0.0');
// Can't timeout in this context on Windows, ping localhost to delay
repo.exec(windows ? 'ping 127.0.0.1 -n 2' : 'sleep 2');
repo.exec('git tag v1.0.0 HEAD~1');
repo.makeCommit('Fourth Commit'); // 0.0.1+2
const result = await repo.runAction();
expect(result.formattedVersion).toBe('2.0.1+0');
}, 15000);
test('Change detection is true by default', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.makeCommit(`Second Commit`); // 0.0.2
const result = await repo.runAction();
expect(result.changed).toBe(true);
}, 15000);
test('Changes to monitored path is true when change is in path', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.exec('mkdir project1');
repo.makeCommit(`Second Commit`, 'project1'); // 0.0.2
const result = await repo.runAction({ changePath: "project1" });
expect(result.changed).toBe(true);
}, 15000);
test('Changes to monitored path is false when changes are not in path', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.exec('mkdir project1');
repo.exec('mkdir project2');
repo.makeCommit(`Second Commit`, 'project2'); // 0.0.2
const result = await repo.runAction({ changePath: "project1" });
expect(result.changed).toBe(false);
}, 15000);
test('Changes can be detected without tags', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('mkdir project1');
repo.makeCommit(`Second Commit`, 'project1'); // 0.0.2
const result = await repo.runAction({ changePath: "project1" });
expect(result.changed).toBe(true);
}, 15000);
test('Changes to multiple monitored path is true when change is in path', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.exec('mkdir project1');
repo.exec('mkdir project2');
repo.makeCommit(`Second Commit`, 'project2'); // 0.0.2
const result = await repo.runAction({ changePath: "project1 project2" });
expect(result.changed).toBe(true);
}, 15000);
test('Changes to multiple monitored path is false when change is not in path', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.exec('mkdir project1');
repo.exec('mkdir project2');
repo.exec('mkdir project3');
repo.makeCommit(`Second Commit`, 'project3'); // 0.0.2
const result = await repo.runAction({ changePath: "project1 project2" });
expect(result.changed).toBe(false);
}, 15000);
test('Namespace is tracked separately', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit'); // 0.0.1
repo.exec('git tag 0.0.1');
repo.makeCommit('Second Commit'); // 0.0.2
repo.exec('git tag 0.1.0-subproject');
repo.makeCommit('Third Commit'); // 0.0.2 / 0.1.1
const result = await repo.runAction();
const subprojectResult = await repo.runAction({ namespace: "subproject" });
expect(result.formattedVersion).toBe('0.0.2+1');
expect(subprojectResult.formattedVersion).toBe('0.1.1+0');
}, 15000);
test('Commits outside of path are not counted', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit');
repo.makeCommit('Second Commit');
repo.makeCommit('Third Commit');
const result = await repo.runAction({ changePath: "project1" });
expect(result.formattedVersion).toBe('0.0.0+0');
}, 15000);
test('Commits inside path are counted', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit');
repo.makeCommit('Second Commit');
repo.makeCommit('Third Commit');
repo.exec('mkdir project1');
repo.makeCommit('Fourth Commit', 'project1'); // 0.0.1+0
repo.makeCommit('Fifth Commit', 'project1'); // 0.0.1+1
repo.makeCommit('Sixth Commit', 'project1'); // 0.0.1+2
const result = await repo.runAction({ changePath: "project1" });
expect(result.formattedVersion).toBe('0.0.1+2');
}, 15000);
test('Current tag is used', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.0
repo.makeCommit('Initial Commit');
repo.makeCommit('Second Commit');
repo.makeCommit('Third Commit');
repo.exec('git tag 7.6.5');
const result = await repo.runAction();
expect(result.formattedVersion).toBe('7.6.5+0');
}, 15000);
test('Bump each commit works', async () => {
const repo = createTestRepo({ tagPrefix: '', bumpEachCommit: true }); // 0.0.0
expect((await repo.runAction()).formattedVersion).toBe('0.0.0+0');
repo.makeCommit('Initial Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1+0');
repo.makeCommit('Second Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.2+0');
repo.makeCommit('Third Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.3+0');
repo.makeCommit('Fourth Commit (MINOR)');
expect((await repo.runAction()).formattedVersion).toBe('0.1.0+0');
repo.makeCommit('Fifth Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.1.1+0');
repo.makeCommit('Sixth Commit (MAJOR)');
expect((await repo.runAction()).formattedVersion).toBe('1.0.0+0');
repo.makeCommit('Seventh Commit');
expect((await repo.runAction()).formattedVersion).toBe('1.0.1+0');
}, 15000);
test('Bump each commit picks up tags', async () => {
const repo = createTestRepo({ tagPrefix: '', bumpEachCommit: true }); // 0.0.0
expect((await repo.runAction()).formattedVersion).toBe('0.0.0+0');
repo.makeCommit('Initial Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1+0');
repo.makeCommit('Second Commit');
expect((await repo.runAction()).formattedVersion).toBe('0.0.2+0');
repo.makeCommit('Third Commit');
repo.exec('git tag 3.0.0');
expect((await repo.runAction()).formattedVersion).toBe('3.0.0+0');
repo.makeCommit('Fourth Commit');
expect((await repo.runAction()).formattedVersion).toBe('3.0.1+0');
}, 15000);
test('Increment not affected by matching tag', async () => {
const repo = createTestRepo({ tagPrefix: '' }); // 0.0.1
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit'); // 0.0.1+1
repo.exec('git tag 0.0.1');
expect((await repo.runAction()).formattedVersion).toBe('0.0.1+1');
}, 15000);
test('Regular expressions can be used as major tag', async () => {
const repo = createTestRepo({ tagPrefix: '', majorPattern: '/S[a-z]+Value/' }); // 0.0.1
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit SomeValue'); // 1.0.0+0
expect((await repo.runAction()).formattedVersion).toBe('1.0.0+0');
}, 15000);
test('Regular expressions can be used as minor tag', async () => {
const repo = createTestRepo({ tagPrefix: '', minorPattern: '/S[a-z]+Value/' }); // 0.0.1
repo.makeCommit('Initial Commit'); // 0.0.1+0
repo.makeCommit('Second Commit SomeValue'); // 0.0.1+1
expect((await repo.runAction()).formattedVersion).toBe('0.1.0+0');
}, 15000);
test('Tag prefix can include forward slash', async () => {
const repo = createTestRepo({ tagPrefix: 'version/' }); // 0.0.0
repo.makeCommit('Initial Commit');
repo.exec('git tag version/1.2.3');
const result = await repo.runAction();
expect(result.formattedVersion).toBe('1.2.3+0');
}, 15000);

59
src/main.ts Normal file
View file

@ -0,0 +1,59 @@
import { runAction } from './action';
import { ActionConfig } from './ActionConfig';
import { ConfigurationProvider } from './ConfigurationProvider';
import { VersionResult } from './VersionResult';
import * as core from '@actions/core';
function setOutput(versionResult: VersionResult) {
const { major, minor, patch, increment, formattedVersion, versionTag, changed, authors, currentCommit } = versionResult;
const repository = process.env.GITHUB_REPOSITORY;
if (!changed) {
core.info('No changes detected for this commit');
}
core.info(`Version is ${formattedVersion}`);
if (repository !== undefined) {
core.info(`To create a release for this version, go to https://github.com/${repository}/releases/new?tag=${versionTag}&target=${currentCommit.split('/').slice(-1)[0]}`);
}
core.setOutput("version", formattedVersion);
core.setOutput("major", major.toString());
core.setOutput("minor", minor.toString());
core.setOutput("patch", patch.toString());
core.setOutput("increment", increment.toString());
core.setOutput("changed", changed.toString());
core.setOutput("version_tag", versionTag);
core.setOutput("authors", authors);
}
export async function run() {
const config: ActionConfig = {
branch: core.getInput('branch'),
tagPrefix: core.getInput('tag_prefix'),
majorPattern: core.getInput('major_pattern'),
minorPattern: core.getInput('minor_pattern'),
versionFormat: core.getInput('version_format'),
changePath: core.getInput('change_path'),
namespace: core.getInput('namespace'),
bumpEachCommit: core.getInput('bump_each_commit') === 'true',
searchCommitBody: core.getInput('search_commit_body') === 'true',
userFormatType: core.getInput('user_format_type')
};
if (config.versionFormat === '' && core.getInput('format') !== '') {
core.warning(`The 'format' input is deprecated, use 'versionFormat' instead`);
config.versionFormat = core.getInput('format');
}
if (core.getInput('short_tags') !== '') {
core.warning(`The 'short_tags' input option is no longer supported`);
}
const configurationProvider = new ConfigurationProvider(config);
const result = await runAction(configurationProvider);
setOutput(result);
}
run();

View file

@ -0,0 +1,47 @@
import { ActionConfig } from "../ActionConfig";
import { CommitInfo } from "./CommitInfo";
import { CommitInfoSet } from "./CommitInfoSet";
import { DefaultVersionClassifier } from "./DefaultVersionClassifier";
import { ReleaseInformation } from "./ReleaseInformation";
import { VersionClassification } from "./VersionClassification";
import { VersionType } from "./VersionType";
export class BumpAlwaysVersionClassifier extends DefaultVersionClassifier {
constructor(config: ActionConfig) {
super(config);
// Placeholder for consistency
}
public override async ClassifyAsync(lastRelease: ReleaseInformation, commitSet: CommitInfoSet): Promise<VersionClassification> {
if (lastRelease.currentPatch !== null) {
return new VersionClassification(VersionType.None, 0, false, <number>lastRelease.currentMajor, <number>lastRelease.currentMinor, <number>lastRelease.currentPatch);
}
let { major, minor, patch } = lastRelease;
let type = VersionType.None;
if (commitSet.commits.length === 0) {
return new VersionClassification(type, 0, false, major, minor, patch);
}
for (let commit of commitSet.commits.reverse()) {
if (this.majorPattern(commit)) {
major += 1;
minor = 0;
patch = 0;
type = VersionType.Major;
} else if (this.minorPattern(commit)) {
minor += 1;
patch = 0;
type = VersionType.Minor;
} else {
patch += 1;
type = VersionType.Patch;
}
}
return new VersionClassification(type, 0, true, major, minor, patch);
}
}

View file

@ -0,0 +1,27 @@
/** Represents information about a commit */
export class CommitInfo {
/**
* Creates a new commit information instance
* @param hash - The hash of the commit
* @param subject - The subject of the commit message
* @param body - The body of the commit message
* @param author - The author's name
* @param authorEmail - The author's email
* @param authorDate - The date the commit was authored
* @param committer - The committer's name
* @param committerEmail - The committer's email
* @param committerDate - The date the commit was committed
* @param tags - List of any tags associated with this commit
*/
constructor(
public hash: string,
public subject: string,
public body: string,
public author: string,
public authorEmail: string,
public authorDate: Date,
public committer: string,
public committerEmail: string,
public committerDate: Date,
public tags: string[]) { }
}

View file

@ -0,0 +1,9 @@
import { CommitInfo } from "./CommitInfo";
/** Represents information about a set of commits */
export class CommitInfoSet {
constructor(
public changed: boolean,
public commits: CommitInfo[]
){}
}

View file

@ -0,0 +1,14 @@
import { CommitInfoSet } from "./CommitInfoSet";
/**
* Defines a provider to retrieve commit information for a range of commits
*/
export interface CommitsProvider {
/**
* Gets the commit information for a range of commits
* @param startHash - The hash of commit of the last release, result should be exclusive
* @param endHash - The hash of the current commit, result should be inclusive
*/
GetCommitsAsync(startHash: string, endHash: string): Promise<CommitInfoSet>;
}

View file

@ -0,0 +1,13 @@
/** Resolver to obtain information about the repository */
export interface CurrentCommitResolver {
/**
* Resolves the current commit.
* @returns The current commit.
*/
ResolveAsync(): Promise<string>;
/**
* Returns true if the repository is empty
* @returns True if the repository is empty
*/
IsEmptyRepoAsync(): Promise<boolean>;
}

View file

@ -0,0 +1,94 @@
import { ActionConfig } from "../ActionConfig";
import { cmd } from "../CommandRunner";
import { CommitInfo } from "./CommitInfo";
import { CommitInfoSet } from "./CommitInfoSet";
import { CommitsProvider } from "./CommitsProvider";
export class DefaultCommitsProvider implements CommitsProvider {
private changePath: string;
constructor(config: ActionConfig) {
this.changePath = config.changePath;
}
async GetCommitsAsync(startHash: string, endHash: string): Promise<CommitInfoSet> {
const logSplitter = `@@@START_RECORD`
const formatPlaceholders = Object.entries({
hash: '%H',
subject: '%s',
body: '%b',
author: '%an',
authorEmail: '%ae',
authorDate: '%aI',
committer: '%cn',
committerEmail: '%ce',
committerDate: '%cI',
tags: '%d'
});
const pretty = logSplitter + '%n' + formatPlaceholders
.map(x => `@@@${x[0]}%n${x[1]}`)
.join('%n');
var logCommand = `git log --pretty="${pretty}" --author-date-order ${(startHash === '' ? endHash : `${startHash}..${endHash}`)}`;
if (this.changePath !== '') {
logCommand += ` -- ${this.changePath}`;
}
const log = await cmd(logCommand);
const entries = log
.split(logSplitter)
.slice(1);
const commits = entries.map(entry => {
const fields: any = entry
.split(`@@@`)
.slice(1)
.reduce((acc: any, value: string) => {
const firstLine = value.indexOf('\n');
const key = value.substring(0, firstLine);
acc[key] = value.substring(firstLine + 1).trim();
return acc;
}, {});
const tags = fields.tags
.split(',')
.map((v: string) => v.trim())
.filter((v: string) => v.startsWith('tags: '))
.map((v: string) => v.substring(5).trim());
return new CommitInfo(
fields.hash,
fields.subject,
fields.body,
fields.author,
fields.authorEmail,
new Date(fields.authorDate),
fields.committer,
fields.committerEmail,
new Date(fields.committerDate),
tags
);
});
// check for changes
let changed = true;
if (this.changePath !== '') {
if (startHash === '') {
const changedFiles = await cmd(`git log --name-only --oneline ${endHash} -- ${this.changePath}`);
changed = changedFiles.length > 0;
} else {
const changedFiles = await cmd(`git diff --name-only ${startHash}..${endHash} -- ${this.changePath}`);
changed = changedFiles.length > 0;
}
}
return new CommitInfoSet(changed, commits);
}
}

View file

@ -0,0 +1,25 @@
import { ActionConfig } from "../ActionConfig";
import { cmd } from "../CommandRunner";
import { CurrentCommitResolver } from "./CurrentCommitResolver";
export class DefaultCurrentCommitResolver implements CurrentCommitResolver {
private branch: string;
constructor(config: ActionConfig) {
this.branch = config.branch;
}
public async ResolveAsync(): Promise<string> {
if (this.branch === 'HEAD') {
return (await cmd('git', 'rev-parse', 'HEAD')).trim();
}
return this.branch;
}
public async IsEmptyRepoAsync(): Promise<boolean> {
let lastCommitAll = (await cmd('git', 'rev-list', '-n1', '--all')).trim();
return lastCommitAll === '';
}
}

View file

@ -0,0 +1,23 @@
import { ActionConfig } from "../ActionConfig";
import { CommitInfo } from "./CommitInfo";
import { CommitInfoSet } from "./CommitInfoSet";
import { DefaultVersionClassifier } from "./DefaultVersionClassifier";
import { ReleaseInformation } from "./ReleaseInformation";
test('Regular expressions can be used as minor tag direct', async () => {
const classifier = new DefaultVersionClassifier({ ...new ActionConfig(), ...{ tagPrefix: '', minorPattern: '/S[a-z]+Value/' }});
const releaseInfo =new ReleaseInformation(0,0,1,"",null,null,null);
const commitSet = new CommitInfoSet(false, [
new CommitInfo("", "Second Commit SomeValue", "", "","", new Date(), "", "", new Date(), []),
new CommitInfo("", "Initial Commit", "", "","", new Date(), "", "", new Date(), []),
]);
const result = await classifier.ClassifyAsync(releaseInfo, commitSet);
expect(result.major).toBe(0);
expect(result.minor).toBe(1);
expect(result.patch).toBe(0);
expect(result.increment).toBe(0);
});

View file

@ -0,0 +1,99 @@
import { ActionConfig } from "../ActionConfig";
import { CommitInfo } from "./CommitInfo";
import { CommitInfoSet } from "./CommitInfoSet";
import { ReleaseInformation } from "./ReleaseInformation";
import { VersionClassification } from "./VersionClassification";
import { VersionClassifier } from "./VersionClassifier";
import { VersionType } from "./VersionType";
export class DefaultVersionClassifier implements VersionClassifier {
protected majorPattern: (commit: CommitInfo) => boolean;
protected minorPattern: (commit: CommitInfo) => boolean;
constructor(config: ActionConfig) {
const searchBody = config.searchCommitBody;
this.majorPattern = this.parsePattern(config.majorPattern, searchBody);
this.minorPattern = this.parsePattern(config.minorPattern, searchBody);
}
protected parsePattern(pattern: string, searchBody: boolean): (pattern: CommitInfo) => boolean {
if (pattern.startsWith('/') && pattern.endsWith('/')) {
var regex = new RegExp(pattern.slice(1, -1));
return searchBody ?
(commit: CommitInfo) => regex.test(commit.subject) || regex.test(commit.body) :
(commit: CommitInfo) => regex.test(commit.subject);
} else {
const matchString = pattern;
return searchBody ?
(commit: CommitInfo) => commit.subject.includes(matchString) || commit.body.includes(matchString) :
(commit: CommitInfo) => commit.subject.includes(matchString);
}
}
protected getNextVersion(current: ReleaseInformation, type: VersionType): ({ major: number, minor: number, patch: number }) {
switch (type) {
case VersionType.Major:
return { major: current.major + 1, minor: 0, patch: 0 };
case VersionType.Minor:
return { major: current.major, minor: current.minor + 1, patch: 0 };
case VersionType.Patch:
return { major: current.major, minor: current.minor, patch: current.patch + 1 };
case VersionType.None:
return { major: current.major, minor: current.minor, patch: current.patch };
default:
throw new Error(`Unknown change type: ${type}`);
}
}
private resolveCommitType(commitsSet: CommitInfoSet): ({ type: VersionType, increment: number, changed: boolean }) {
if (commitsSet.commits.length === 0) {
return { type: VersionType.None, increment: 0, changed: commitsSet.changed };
}
const commits = commitsSet.commits.reverse();
let index = 1;
for (let commit of commits) {
if (this.majorPattern(commit)) {
return { type: VersionType.Major, increment: commits.length - index, changed: commitsSet.changed };
}
index++;
}
index = 1;
for (let commit of commits) {
if (this.minorPattern(commit)) {
return { type: VersionType.Minor, increment: commits.length - index, changed: commitsSet.changed };
}
index++;
}
return { type: VersionType.Patch, increment: commitsSet.commits.length - 1, changed: true };
}
public async ClassifyAsync(lastRelease: ReleaseInformation, commitSet: CommitInfoSet): Promise<VersionClassification> {
const { type, increment, changed } = this.resolveCommitType(commitSet);
const { major, minor, patch } = this.getNextVersion(lastRelease, type);
if (lastRelease.currentPatch !== null) {
// If the current commit is tagged, we must use that version. Here we check if the version we have resolved from the
// previous commits is the same as the current version. If it is, we will use the increment value, otherwise we reset
// to zero. For example:
// - commit 1 - v1.0.0+0
// - commit 2 - v1.0.0+1
// - commit 3 was tagged v2.0.0 - v2.0.0+0
// - commit 4 - v2.0.1+0
const versionsMatch = lastRelease.currentMajor === major && lastRelease.currentMinor === minor && lastRelease.currentPatch === patch;
const currentIncremement = versionsMatch ? increment : 0;
return new VersionClassification(VersionType.None, currentIncremement, false, <number>lastRelease.currentMajor, <number>lastRelease.currentMinor, <number>lastRelease.currentPatch);
}
return new VersionClassification(type, increment, changed, major, minor, patch);
}
}

View file

@ -0,0 +1,6 @@
import { TagFormatter } from '../formatting/TagFormatter';
import { ReleaseInformation } from './ReleaseInformation';
export interface LastReleaseResolver {
ResolveAsync(current: string, tagFormatter: TagFormatter): Promise<ReleaseInformation>;
}

View file

@ -0,0 +1,21 @@
// Finds the hash of the last commit
export class ReleaseInformation {
/**
* Creates a new instance
* @param major - the major version number
* @param minor - the minor version number
* @param patch - the patch version number
* @param hash - the hash of commit of the last release
* @param currentMajor - the major version number from the current commit
* @param currentMinor - the minor version number from the current commit
* @param currentPatch - the patch version number from the current commit
*/
constructor(
public major: number,
public minor: number,
public patch: number,
public hash: string,
public currentMajor: number | null,
public currentMinor: number | null,
public currentPatch: number | null,) { }
}

View file

@ -0,0 +1,59 @@
import { cmd } from "../CommandRunner";
import { TagFormatter } from "../formatting/TagFormatter";
import { LastReleaseResolver } from "./LastReleaseResolver";
import { ReleaseInformation } from "./ReleaseInformation";
import { ActionConfig } from "../ActionConfig";
import * as core from '@actions/core';
export class TagLastReleaseResolver implements LastReleaseResolver {
private changePath: string;
constructor(config: ActionConfig) {
this.changePath = config.changePath;
}
async ResolveAsync(current: string, tagFormatter: TagFormatter): Promise<ReleaseInformation> {
const releasePattern = tagFormatter.GetPattern();
let currentTag = (await cmd(
`git tag --points-at ${current} ${releasePattern}`
)).trim();
const [currentMajor, currentMinor, currentPatch] = !!currentTag ? tagFormatter.Parse(currentTag) : [null, null, null];
let tag = '';
try {
tag = (await cmd(
'git',
`describe`,
`--tags`,
`--abbrev=0`,
`--match=${releasePattern}`,
`${current}~1`
)).trim();
}
catch (err) {
tag = '';
}
if (tag === '') {
if (await cmd('git', 'remote') !== '') {
// Since there is no remote, we assume that there are no other tags to pull. In
// practice this isn't likely to happen, but it keeps the test output from being
// polluted with a bunch of warnings.
core.warning('No tags are present for this repository. If this is unexpected, check to ensure that tags have been pulled from the remote.');
}
// no release tags yet, use the initial commit as the root
return new ReleaseInformation(0, 0, 0, '', currentMajor, currentMinor, currentPatch);
}
// parse the version tag
const [major, minor, patch] = tagFormatter.Parse(tag);
const root = await cmd('git', `merge-base`, tag, current);
return new ReleaseInformation(major, minor, patch, root.trim(), currentMajor, currentMinor, currentPatch);
}
}

14
src/providers/UserInfo.ts Normal file
View file

@ -0,0 +1,14 @@
/** Represents information about a user (e.g. committer, author, tagger) */
export class UserInfo {
/**
* Creates a new instance
* @param name - User's name
* @param email - User's email
* @param commits - Number of commits in the scope evaluated
*/
constructor(
public name: string,
public email: string,
public commits: number) { }
}

View file

@ -0,0 +1,21 @@
import { VersionType } from './VersionType';
/** The result of a version classification */
export class VersionClassification {
/**
* Creates a new version classification result instance
* @param type - The type of change the current range represents
* @param increment - The number of commits which have this version, usually zero-based
* @param changed - True if the version has changed, false otherwise
* @param major - The major version number
* @param minor - The minor version number
* @param patch - The patch version number
*/
constructor(
public type: VersionType,
public increment: number,
public changed: boolean,
public major: number,
public minor: number,
public patch: number) { }
}

View file

@ -0,0 +1,17 @@
import { CommitInfoSet } from "./CommitInfoSet";
import { ReleaseInformation } from "./ReleaseInformation";
import { VersionClassification } from "./VersionClassification";
/**
* Defines the 'business logic' to turn commits into parameters for creating
* output values
*/
export interface VersionClassifier {
/**
* Produces the version classification for a given commit set
* @param lastRelease - The last release information
* @param commitSet - The commits to classify, ordered from most recent to oldest
* @returns - The version classification
*/
ClassifyAsync(lastRelease: ReleaseInformation, commitSet: CommitInfoSet): Promise<VersionClassification>;
}

View file

@ -0,0 +1,27 @@
import { CommitInfo } from "./CommitInfo";
import { VersionType } from "./VersionType";
/**
* Represents the "resolved" information about a version change, serves
* as the input to formatters that produce the final output
*/
export class VersionInformation {
/**
* Creates a new version information instance
* @param major - The major version number
* @param minor - The minor version number
* @param patch - The patch version number
* @param increment - The number of commits for this version
* @param type - The type of change the current range represents
* @param commits - The list of commits for this version
* @param changed - True if the version has changed, false otherwise
*/
constructor(
public major: number,
public minor: number,
public patch: number,
public increment: number,
public type: VersionType,
public commits: CommitInfo[],
public changed: boolean) { }
}

View file

@ -0,0 +1,12 @@
/** Indicates the type of change a particular version change represents */
export enum VersionType {
/** Indicates a major version change */
Major,
/** Indicates a minor version change */
Minor,
/** Indicates a patch version change */
Patch,
/** Indicates no change--generally this means that the current commit is already tagged with a version */
None
}

12
tsconfig.json Normal file
View file

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"outDir": "./lib", /* Redirect output structure to the directory. */
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
},
"exclude": ["node_modules", "**/*.test.ts"]
}