Add namespace & improve subproject support (MAJOR)

This commit is contained in:
Paul Hatcherian 2020-09-02 07:48:21 -04:00
parent 1ce73af6aa
commit ef6fe2de1c
4 changed files with 140 additions and 39 deletions

View file

@ -27,6 +27,9 @@ inputs:
change_path: change_path:
description: "Path to check for changes. If any changes are detected in the path the 'changed' output will true. Enter multiple paths separated by spaces." description: "Path to check for changes. If any changes are detected in the path the 'changed' output will true. Enter multiple paths separated by spaces."
required: false required: false
namespace:
description: "Use to create a named sub-version. This value will be appended to tags created for this version."
required: false
outputs: outputs:
major: major:
description: "Current major number" description: "Current major number"
@ -39,7 +42,7 @@ outputs:
version: version:
description: "The version result, in the format {major}.{minor}.{patch}" description: "The version result, in the format {major}.{minor}.{patch}"
changed: 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 false." 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: runs:
using: "node12" using: "node12"
main: "dist/index.js" main: "dist/index.js"

View file

@ -5,20 +5,26 @@ const eol = require('os').EOL;
const tagPrefix = core.getInput('tag_prefix') || ''; const tagPrefix = core.getInput('tag_prefix') || '';
const cmd = async (command, ...args) => { const cmd = async (command, ...args) => {
let output = ''; let output = '', errors = '';
const options = { const options = {
silent: true silent: true
}; };
options.listeners = { options.listeners = {
stdout: (data) => { output += data.toString(); } stdout: (data) => { output += data.toString(); },
stderr: (data) => { errors += data.toString(); }
}; };
await exec.exec(command, args, options) await exec.exec(command, args, options)
.catch(err => { core.error(`${command} ${args.join(' ')} failed: ${err}`); throw err; }); .catch(err => { core.warning(`${command} ${args.join(' ')} failed: ${err}`); });
if (errors !== '') {
core.warning(errors);
}
return output; return output;
}; };
const setOutput = (major, minor, patch, increment, changed, branch) => { const setOutput = (major, minor, patch, increment, changed, branch, namespace) => {
const format = core.getInput('format', { required: true }); const format = core.getInput('format', { required: true });
var version = format var version = format
.replace('${major}', major) .replace('${major}', major)
@ -26,6 +32,10 @@ const setOutput = (major, minor, patch, increment, changed, branch) => {
.replace('${patch}', patch) .replace('${patch}', patch)
.replace('${increment}', increment); .replace('${increment}', increment);
if (namespace !== '') {
version += `-${namespace}`
}
const tag = tagPrefix + version; const tag = tagPrefix + version;
const repository = process.env.GITHUB_REPOSITORY; const repository = process.env.GITHUB_REPOSITORY;
@ -52,17 +62,17 @@ async function run() {
const majorPattern = core.getInput('major_pattern', { required: true }); const majorPattern = core.getInput('major_pattern', { required: true });
const minorPattern = core.getInput('minor_pattern', { required: true }); const minorPattern = core.getInput('minor_pattern', { required: true });
const changePath = core.getInput('change_path') || ''; const changePath = core.getInput('change_path') || '';
const namespace = core.getInput('namespace') || '';
const releasePattern = `${tagPrefix}*`; const releasePattern = namespace === '' ? `${tagPrefix}*[0-9.]` : `${tagPrefix}*[0-9.]-${namespace}`;
let major = 0, minor = 0, patch = 0, increment = 0; let major = 0, minor = 0, patch = 0, increment = 0;
let changed = true; let changed = true;
let lastCommitAll = (await cmd('git', 'rev-list', '-n1', '--all')).trim(); let lastCommitAll = (await cmd('git', 'rev-list', '-n1', '--all')).trim();
if (lastCommitAll === '') { if (lastCommitAll === '') {
// empty repo // empty repo
setOutput('0', '0', '0', '0', changed, branch); setOutput('0', '0', '0', '0', changed, branch, namespace);
return; return;
} }
@ -95,6 +105,7 @@ async function run() {
let tagParts = tag.split('/'); let tagParts = tag.split('/');
let versionValues = tagParts[tagParts.length - 1] let versionValues = tagParts[tagParts.length - 1]
.substr(tagPrefix.length) .substr(tagPrefix.length)
.slice(0, namespace === '' ? 999 : -(namespace.length + 1))
.split('.'); .split('.');
major = parseInt(versionValues[0]); major = parseInt(versionValues[0]);
@ -102,23 +113,27 @@ async function run() {
patch = versionValues.length > 2 ? parseInt(versionValues[2]) : 0; patch = versionValues.length > 2 ? parseInt(versionValues[2]) : 0;
if (isNaN(major) || isNaN(minor) || isNaN(patch)) { if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
throw `Invalid tag ${tag}`; core.setFailed(`Invalid tag ${tag} (${versionValues})`);
return;
} }
root = await cmd('git', `merge-base`, tag, branch); root = await cmd('git', `merge-base`, tag, branch);
} }
root = root.trim(); root = root.trim();
const log = await cmd( var logCommand = `git log --pretty="%s" --author-date-order ${(root === '' ? branch : `${root}..${branch}`)}`;
'git',
'log',
'--pretty="%s"',
'--author-date-order',
root === '' ? branch : `${root}..${branch}`);
if (changePath !== '') { if (changePath !== '') {
const changedFiles = await cmd(`git diff --name-only ${root}..${branch} -- ${changePath}`); logCommand += ` -- ${changePath}`;
}
const log = await cmd(logCommand);
core.info("LOG:\n" + log)
if (changePath !== '') {
const changedFiles = await cmd(`git diff --name-only ${(root === '' ? branch : `${root}..${branch}`)} -- ${changePath}`);
changed = changedFiles.length > 0; changed = changedFiles.length > 0;
} }
@ -147,7 +162,7 @@ async function run() {
patch++; patch++;
} }
setOutput(major, minor, patch, increment, changed, branch); setOutput(major, minor, patch, increment, changed, branch, namespace);
} catch (error) { } catch (error) {
core.error(error); core.error(error);

View file

@ -3,11 +3,13 @@ const path = require('path');
const process = require('process'); const process = require('process');
// Action input variables // Action input variables
process.env['INPUT_BRANCH'] = "master"; const defaultInputs = {
process.env['INPUT_TAG_PREFIX'] = "v"; branch: "master",
process.env['INPUT_MAJOR_PATTERN'] = "(MAJOR)"; tag_prefix: "v",
process.env['INPUT_MINOR_PATTERN'] = "(MINOR)"; major_pattern: "(MAJOR)",
process.env['INPUT_FORMAT'] = "${major}.${minor}.${patch}"; minor_pattern: "(MINOR)",
format: "${major}.${minor}.${patch}"
};
// Creates a randomly named git repository and returns a function to execute commands in it // Creates a randomly named git repository and returns a function to execute commands in it
const createTestRepo = (inputs) => { const createTestRepo = (inputs) => {
@ -15,7 +17,7 @@ const createTestRepo = (inputs) => {
cp.execSync(`mkdir ${repoDirectory} && git init ${repoDirectory}`); cp.execSync(`mkdir ${repoDirectory} && git init ${repoDirectory}`);
const run = (command, extraInputs) => { const run = (command, extraInputs) => {
const allInputs = Object.assign({}, inputs, extraInputs); const allInputs = Object.assign({ ...defaultInputs }, inputs, extraInputs);
let env = {}; let env = {};
for (let key in allInputs) { for (let key in allInputs) {
env[`INPUT_${key.toUpperCase()}`] = allInputs[key]; env[`INPUT_${key.toUpperCase()}`] = allInputs[key];
@ -300,19 +302,6 @@ test('Change detection is true by default', () => {
repo.clean(); 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', () => { test('Changes to monitored path is true when change is in path', () => {
const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0 const repo = createTestRepo({ tag_prefix: '' }); // 0.0.0
@ -370,5 +359,55 @@ test('Changes to multiple monitored path is false when change is not in path', (
expect(result).toMatch('::set-output name=changed::false'); 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(); repo.clean();
}); });

View file

@ -18,18 +18,62 @@ message alters the type of change the next version will represent.
<!-- start usage --> <!-- start usage -->
```yaml ```yaml
- uses: paulhatch/semantic-version@v2.1.1 - uses: paulhatch/semantic-version@v3.0.0
with: with:
# The branch to count commits on # The branch to count commits on
branch: "master" branch: "master"
# The prefix to use to identify tags # The prefix to use to identify tags
tag_prefix: "v" tag_prefix: "v"
# A string which, if present in a git commit, indicates that a change represents a major (breaking) change # A string which, if present in a git commit, indicates that a change represents a
# major (breaking) change
major_pattern: "(MAJOR)" major_pattern: "(MAJOR)"
# Same as above except indicating a minor change # Same as above except indicating a minor change
minor_pattern: "(MINOR)" minor_pattern: "(MINOR)"
# A string to determine the format of the version output # A string to determine the format of the version output
format: "${major}.${minor}.${patch}-prerelease.${increment}" format: "${major}.${minor}.${patch}-prerelease.${increment}"
# Path to check for changes. If any changes are detected in the path the 'changed' output will true. Enter multiple paths separated by spaces. # Optional path to check for changes. If any changes are detected in the path the
# 'changed' output will true. Enter multiple paths separated by spaces.
change_path: "src/my-service" change_path: "src/my-service"
# Named version, will be used as suffix for name version tag
namespace: project-b
```
## Using Multiple Versions in the Same Repository
It is possible to create additional versions for multiple project co-existing
in one repository, for example you may have a Helm chart, database migration,
or simply be hosting multiple projects in the same repository and want them to
be versioned independently. There are a few settings that can be used to
accomplish this:
First, you can set the `change_path` input to specify a path that will be
inspected for changes. Commits which do no change any files in this path will
not increase the `increment` output. In addition, if there are no changes in
a given commit with this path specified, the `changed` value will be false.
Second, the input `namespace` can be set to create an additional named version.
If this value is set, it will be appended to the end of tags for the version,
and only tags with this value appended will be considered when determining the
version.
Finally, set different values for `major_pattern` and `minor_pattern` than the
other projects in order to be able to mark these commits independently.
To use secondary versions in a workflow, simply create additional steps in a
job referencing semantic version multiple times. For example:
```yaml
- name: Application Version
id: version
uses: paulhatch/semantic-version@v3.0.0
with:
change_path: "src/service"
- name: Database Version
id: db-version
uses: paulhatch/semantic-version@v3.0.0
with:
major_pattern: "(MAJOR-DB)"
minor_pattern: "(MINOR-DB)"
change_path: "src/migrations"
namespace: db
``` ```