diff --git a/action.yml b/action.yml index d2de56c..87e2e90 100644 --- a/action.yml +++ b/action.yml @@ -27,6 +27,9 @@ inputs: 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." 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: major: description: "Current major number" @@ -39,7 +42,7 @@ outputs: version: description: "The version result, in the format {major}.{minor}.{patch}" 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: using: "node12" main: "dist/index.js" diff --git a/index.js b/index.js index ceda0e1..c18e62d 100644 --- a/index.js +++ b/index.js @@ -5,20 +5,26 @@ const eol = require('os').EOL; const tagPrefix = core.getInput('tag_prefix') || ''; const cmd = async (command, ...args) => { - let output = ''; + let output = '', errors = ''; const options = { silent: true }; options.listeners = { - stdout: (data) => { output += data.toString(); } + stdout: (data) => { output += data.toString(); }, + stderr: (data) => { errors += data.toString(); } }; 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; }; -const setOutput = (major, minor, patch, increment, changed, branch) => { +const setOutput = (major, minor, patch, increment, changed, branch, namespace) => { const format = core.getInput('format', { required: true }); var version = format .replace('${major}', major) @@ -26,6 +32,10 @@ const setOutput = (major, minor, patch, increment, changed, branch) => { .replace('${patch}', patch) .replace('${increment}', increment); + if (namespace !== '') { + version += `-${namespace}` + } + const tag = tagPrefix + version; const repository = process.env.GITHUB_REPOSITORY; @@ -52,17 +62,17 @@ async function run() { const majorPattern = core.getInput('major_pattern', { required: true }); const minorPattern = core.getInput('minor_pattern', { required: true }); 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 changed = true; - let lastCommitAll = (await cmd('git', 'rev-list', '-n1', '--all')).trim(); if (lastCommitAll === '') { // empty repo - setOutput('0', '0', '0', '0', changed, branch); + setOutput('0', '0', '0', '0', changed, branch, namespace); return; } @@ -95,6 +105,7 @@ async function run() { let tagParts = tag.split('/'); let versionValues = tagParts[tagParts.length - 1] .substr(tagPrefix.length) + .slice(0, namespace === '' ? 999 : -(namespace.length + 1)) .split('.'); major = parseInt(versionValues[0]); @@ -102,23 +113,27 @@ async function run() { patch = versionValues.length > 2 ? parseInt(versionValues[2]) : 0; 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 = root.trim(); - const log = await cmd( - 'git', - 'log', - '--pretty="%s"', - '--author-date-order', - root === '' ? branch : `${root}..${branch}`); + var logCommand = `git log --pretty="%s" --author-date-order ${(root === '' ? branch : `${root}..${branch}`)}`; 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; } @@ -147,7 +162,7 @@ async function run() { patch++; } - setOutput(major, minor, patch, increment, changed, branch); + setOutput(major, minor, patch, increment, changed, branch, namespace); } catch (error) { core.error(error); diff --git a/index.test.js b/index.test.js index 33d99b5..e85f9cf 100644 --- a/index.test.js +++ b/index.test.js @@ -3,11 +3,13 @@ const path = require('path'); const process = require('process'); // Action input variables -process.env['INPUT_BRANCH'] = "master"; -process.env['INPUT_TAG_PREFIX'] = "v"; -process.env['INPUT_MAJOR_PATTERN'] = "(MAJOR)"; -process.env['INPUT_MINOR_PATTERN'] = "(MINOR)"; -process.env['INPUT_FORMAT'] = "${major}.${minor}.${patch}"; +const defaultInputs = { + branch: "master", + tag_prefix: "v", + major_pattern: "(MAJOR)", + minor_pattern: "(MINOR)", + format: "${major}.${minor}.${patch}" +}; // Creates a randomly named git repository and returns a function to execute commands in it const createTestRepo = (inputs) => { @@ -15,7 +17,7 @@ const createTestRepo = (inputs) => { cp.execSync(`mkdir ${repoDirectory} && git init ${repoDirectory}`); const run = (command, extraInputs) => { - const allInputs = Object.assign({}, inputs, extraInputs); + const allInputs = Object.assign({ ...defaultInputs }, inputs, extraInputs); let env = {}; for (let key in allInputs) { env[`INPUT_${key.toUpperCase()}`] = allInputs[key]; @@ -300,19 +302,6 @@ test('Change detection is true by default', () => { 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 @@ -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'); + 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(); }); \ No newline at end of file diff --git a/readme.md b/readme.md index 610f60a..f734982 100644 --- a/readme.md +++ b/readme.md @@ -18,18 +18,62 @@ message alters the type of change the next version will represent. ```yaml -- uses: paulhatch/semantic-version@v2.1.1 +- uses: paulhatch/semantic-version@v3.0.0 with: # The branch to count commits on branch: "master" # The prefix to use to identify tags 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)" # Same as above except indicating a minor change minor_pattern: "(MINOR)" # A string to determine the format of the version output 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" + # 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 ```