diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1d843b4..7511ffd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,8 +1,5 @@ -name: 'Action to test the action locally with act' -on: - push: - branches: - - branch-that-does-not-exist +name: 'Test the action' +on: [ pull_request ] jobs: test-json: @@ -13,13 +10,31 @@ jobs: with: path: "./" + - run: npm ci --prod + - run: touch ./fake-file.log + - run: "git config --global user.email 'changelog@github.com'" + - run: "git config --global user.name 'Awesome Github action'" + - run: "git add . && git commit -m 'feat: Added fake file so version will be bumped'" + - name: Generate changelog id: changelog uses: ./ env: ENV: 'dont-use-git' + EXPECTED_TAG: 'v1.5.0' with: github-token: ${{ secrets.github_token }} + version-file: 'test-file.json' + + - name: Show file + run: | + echo "$( Function in a specified file will be run right before the changelog generation phase, when the next > version is already known, but it was not used anywhere yet. It can be useful if you want to manually update version or tag. -Same restrictions as for the pre-commit hook, but exported function name should be `preChangelogGeneration` +Same restrictions as for the pre-commit hook, but exported functions names should be `preVersionGeneration` for modifications to the version and `preTagGeneration` for modifications to the git tag. Following props will be passed to the function as a single parameter and same output is expected: ```typescript -interface Props { - tag: string; // Next tag e.g. v1.12.3 - version: string; // Next version e.g. 1.12.3 -} +// Next version e.g. 1.12.3 +export function preVersionGeneration(version: string): string {} -export function preChangelogGeneration(props: Props): Props {} +// Next tag e.g. v1.12.3 +export function preTagGeneration(tag: string): string {} ``` ### Config-File-Path diff --git a/action.yml b/action.yml index 58aaca7..96671c9 100644 --- a/action.yml +++ b/action.yml @@ -86,7 +86,8 @@ inputs: fallback-version: description: 'The fallback version, if no older one can be detected, or if it is the first one' default: '0.1.0' - + required: false + config-file-path: description: 'Path to the conventional changelog config file. If set, the preset setting will be ignored' required: false diff --git a/src/helpers/bumpVersion.js b/src/helpers/bumpVersion.js index b7a5882..d67c6b7 100644 --- a/src/helpers/bumpVersion.js +++ b/src/helpers/bumpVersion.js @@ -1,6 +1,8 @@ const core = require('@actions/core') const semverValid = require('semver').valid +const requireScript = require('./requireScript') + /** * Bumps the given version with the given release type * @@ -8,7 +10,7 @@ const semverValid = require('semver').valid * @param version * @returns {string} */ -module.exports = (releaseType, version) => { +module.exports = async (releaseType, version) => { let major, minor, patch if (version) { @@ -44,5 +46,22 @@ module.exports = (releaseType, version) => { core.info(`The version could not be detected, using fallback version '${major}.${minor}.${patch}'.`) } - return `${major}.${minor}.${patch}` + const preChangelogGenerationFile = core.getInput('pre-changelog-generation') + + let newVersion = `${major}.${minor}.${patch}` + + if (preChangelogGenerationFile) { + const preChangelogGenerationScript = requireScript(preChangelogGenerationFile) + + // Double check if we want to update / do something with the version + if (preChangelogGenerationScript && preChangelogGenerationScript.preVersionGeneration) { + const modifiedVersion = await preChangelogGenerationScript.preVersionGeneration(newVersion) + + if (modifiedVersion) { + newVersion = modifiedVersion + } + } + } + + return newVersion } diff --git a/src/helpers/git.js b/src/helpers/git.js index a2c06af..ff80ccb 100644 --- a/src/helpers/git.js +++ b/src/helpers/git.js @@ -1,5 +1,6 @@ const core = require('@actions/core') const exec = require('@actions/exec') +const assert = require('assert') const { GITHUB_REPOSITORY, GITHUB_REF, ENV } = process.env @@ -7,6 +8,8 @@ const branch = GITHUB_REF.replace('refs/heads/', '') module.exports = new (class Git { + commandsRun = [] + constructor() { const githubToken = core.getInput('github-token', { required: true }) @@ -16,10 +19,16 @@ module.exports = new (class Git { const gitUserName = core.getInput('git-user-name') const gitUserEmail = core.getInput('git-user-email') - // if the env is dont-use-git then we mock exec as we are testing a workflow locally + // if the env is dont-use-git then we mock exec as we are testing a workflow if (ENV === 'dont-use-git') { this.exec = (command) => { - console.log(`Skipping "git ${command}" because of test env`) + const fullCommand = `git ${command}` + + console.log(`Skipping "${fullCommand}" because of test env`) + + if (!fullCommand.includes('git remote set-url origin')) { + this.commandsRun.push(fullCommand) + } } } @@ -37,28 +46,14 @@ module.exports = new (class Git { * @param command * @return {Promise<>} */ - exec = command => new Promise(async(resolve, reject) => { - let myOutput = '' - let myError = '' + exec = (command) => new Promise(async(resolve, reject) => { + const exitCode = await exec.exec(`git ${command}`) - const options = { - listeners: { - stdout: (data) => { - myOutput += data.toString() - }, - stderr: (data) => { - myError += data.toString() - }, - }, - } + if (exitCode === 0) { + resolve() - try { - await exec.exec(`git ${command}`, null, options) - - resolve(myOutput) - - } catch (e) { - reject(e) + } else { + reject(`Command "git ${command}" exited with code ${exitCode}.`) } }) @@ -77,18 +72,17 @@ module.exports = new (class Git { * @param file * @returns {*} */ - add = file => this.exec(`add ${file}`) + add = (file) => this.exec(`add ${file}`) /** * Commit all changes * * @param message - * @param args * * @return {Promise<>} */ - commit = (message, args = []) => ( - this.exec(`commit -m "${message}" ${args.join(' ')}`) + commit = (message) => ( + this.exec(`commit -m "${message}"`) ) /** @@ -124,7 +118,7 @@ module.exports = new (class Git { * * @return {Promise<>} */ - isShallow = async () => { + isShallow = async() => { const isShallow = await this.exec('rev-parse --is-shallow-repository') // isShallow does not return anything on local machine @@ -141,7 +135,7 @@ module.exports = new (class Git { * @param repo * @return {Promise<>} */ - updateOrigin = repo => this.exec(`remote set-url origin ${repo}`) + updateOrigin = (repo) => this.exec(`remote set-url origin ${repo}`) /** * Creates git tag @@ -149,6 +143,35 @@ module.exports = new (class Git { * @param tag * @return {Promise<>} */ - createTag = tag => this.exec(`tag -a ${tag} -m "${tag}"`) + createTag = (tag) => this.exec(`tag -a ${tag} -m "${tag}"`) + + /** + * Validates the commands run + */ + testHistory = () => { + if (ENV === 'dont-use-git') { + const { EXPECTED_TAG, SKIPPED_COMMIT } = process.env + + const expectedCommands = [ + 'git config user.name "Conventional Changelog Action"', + 'git config user.email "conventional.changelog.action@github.com"', + 'git rev-parse --is-shallow-repository', + 'git pull --tags --ff-only', + ] + + if (!SKIPPED_COMMIT) { + expectedCommands.push('git add .') + expectedCommands.push(`git commit -m "chore(release): ${EXPECTED_TAG}"`) + } + + expectedCommands.push(`git tag -a ${EXPECTED_TAG} -m "${EXPECTED_TAG}"`) + expectedCommands.push(`git push origin ${branch} --follow-tags`) + + assert.deepStrictEqual( + this.commandsRun, + expectedCommands, + ) + } + } })() diff --git a/src/helpers/requireScript.js b/src/helpers/requireScript.js new file mode 100644 index 0000000..8127b6d --- /dev/null +++ b/src/helpers/requireScript.js @@ -0,0 +1,23 @@ +const core = require('@actions/core') +const path = require('path') +const fs = require('fs') + +/** + * Requires an script + * + * @param file + */ +module.exports = (file) => { + const fileLocation = path.resolve(process.cwd(), file) + + // Double check the script exists before loading it + if (fs.existsSync(fileLocation)) { + core.info(`Loading "${fileLocation}" script`) + + return require(fileLocation) + } + + core.error(`Tried to load "${fileLocation}" script but it does not exists!`) + + return undefined +} diff --git a/src/index.js b/src/index.js index 429084a..f3ceb30 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ const path = require('path') const getVersioning = require('./version') const git = require('./helpers/git') const changelog = require('./helpers/generateChangelog') +const requireScript = require('./helpers/requireScript') async function handleVersioningByExtension(ext, file, versionPath, releaseType) { const versioning = getVersioning(ext) @@ -29,7 +30,7 @@ async function run() { const gitUserEmail = core.getInput('git-user-email') const tagPrefix = core.getInput('tag-prefix') const preset = !core.getInput('config-file-path') ? core.getInput('preset') : '' - const preCommit = core.getInput('pre-commit') + const preCommitFile = core.getInput('pre-commit') const outputFile = core.getInput('output-file') const releaseCount = core.getInput('release-count') const versionFile = core.getInput('version-file') @@ -38,7 +39,7 @@ async function run() { const skipCommit = core.getInput('skip-commit').toLowerCase() === 'true' const skipEmptyRelease = core.getInput('skip-on-empty').toLowerCase() === 'true' const conventionalConfigFile = core.getInput('config-file-path') - const preChangelogGeneration = core.getInput('pre-changelog-generation') + const preChangelogGenerationFile = core.getInput('pre-changelog-generation') core.info(`Using "${preset}" preset`) core.info(`Using "${gitCommitMessage}" as commit message`) @@ -51,12 +52,12 @@ async function run() { core.info(`Using "${outputFile}" as output file`) core.info(`Using "${conventionalConfigFile}" as config file`) - if (preCommit) { - core.info(`Using "${preCommit}" as pre-commit script`) + if (preCommitFile) { + core.info(`Using "${preCommitFile}" as pre-commit script`) } - if (preChangelogGeneration) { - core.info(`Using "${preChangelogGeneration}" as pre-changelog-generation script`) + if (preChangelogGenerationFile) { + core.info(`Using "${preChangelogGenerationFile}" as pre-changelog-generation script`) } core.info(`Skipping empty releases is "${skipEmptyRelease ? 'enabled' : 'disabled'}"`) @@ -65,9 +66,9 @@ async function run() { core.info('Pull to make sure we have the full git history') await git.pull() - const config = conventionalConfigFile && require(path.resolve(process.cwd(), conventionalConfigFile)) + const config = conventionalConfigFile && requireScript(conventionalConfigFile) - conventionalRecommendedBump({ preset, tagPrefix, config }, async (error, recommendation) => { + conventionalRecommendedBump({ preset, tagPrefix, config }, async(error, recommendation) => { if (error) { core.setFailed(error.message) return @@ -91,9 +92,11 @@ async function run() { 'git', versionFile, versionPath, - recommendation.releaseType + recommendation.releaseType, ) + newVersion = versioning.newVersion + } else { const files = versionFile.split(',').map((f) => f.trim()) core.info(`Files to bump: ${files.join(', ')}`) @@ -102,8 +105,9 @@ async function run() { files.map((file) => { const fileExtension = file.split('.').pop() core.info(`Bumping version to file "${file}" with extension "${fileExtension}"`) + return handleVersioningByExtension(fileExtension, file, versionPath, recommendation.releaseType) - }) + }), ) newVersion = versioning[0].newVersion @@ -111,15 +115,16 @@ async function run() { let gitTag = `${tagPrefix}${newVersion}` - if (preChangelogGeneration) { - const newVersionAndTag = await require(path.resolve(process.cwd(), preChangelogGeneration)).preChangelogGeneration({ - tag: gitTag, - version: newVersion, - }) + if (preChangelogGenerationFile) { + const preChangelogGenerationScript = requireScript(preChangelogGenerationFile) - if (newVersionAndTag) { - if (newVersionAndTag.tag) gitTag = newVersionAndTag.tag - if (newVersionAndTag.version) newVersion = newVersionAndTag.version + // Double check if we want to update / do something with the tag + if (preChangelogGenerationScript && preChangelogGenerationScript.preTagGeneration) { + const modifiedTag = await preChangelogGenerationScript.preTagGeneration(gitTag) + + if (modifiedTag) { + gitTag = modifiedTag + } } } @@ -147,12 +152,18 @@ async function run() { if (!skipCommit) { // Add changed files to git - if (preCommit) { - await require(path.resolve(process.cwd(), preCommit)).preCommit({ - tag: gitTag, - version: newVersion, - }) + if (preCommitFile) { + const preCommitScript = requireScript(preCommitFile) + + // Double check if the file exists and the export exists + if (preCommitScript && preCommitScript.preCommit) { + await preCommitScript.preCommit({ + tag: gitTag, + version: newVersion, + }) + } } + await git.add('.') await git.commit(gitCommitMessage.replace('{version}', gitTag)) } @@ -161,11 +172,7 @@ async function run() { await git.createTag(gitTag) core.info('Push all changes') - try { - await git.push() - } catch (error) { - core.setFailed(error.message) - } + await git.push() // Set outputs so other actions (for example actions/create-release) can use it core.setOutput('changelog', stringChangelog) @@ -173,9 +180,19 @@ async function run() { core.setOutput('version', newVersion) core.setOutput('tag', gitTag) core.setOutput('skipped', 'false') + + try { + // If we are running in test mode we use this to validate everything still runs + git.testHistory() + + } catch (error) { + console.error(error) + + core.setFailed(error) + } }) } catch (error) { - core.setFailed(error.message) + core.setFailed(error) } } diff --git a/src/version/git.js b/src/version/git.js index 4077c93..38de4cb 100644 --- a/src/version/git.js +++ b/src/version/git.js @@ -12,11 +12,11 @@ module.exports = new (class Git extends BaseVersioning { gitSemverTags({ tagPrefix, - }, (err, tags) => { + }, async(err, tags) => { const currentVersion = tags.length > 0 ? tags.shift().replace(tagPrefix, '') : null // Get the new version - this.newVersion = bumpVersion( + this.newVersion = await bumpVersion( releaseType, currentVersion, ) diff --git a/src/version/json.js b/src/version/json.js index 7a75c52..4f75ba0 100644 --- a/src/version/json.js +++ b/src/version/json.js @@ -12,7 +12,7 @@ module.exports = new (class Json extends BaseVersioning { * @param {!string} releaseType - The type of release * @return {*} */ - bump = (releaseType) => { + bump = async(releaseType) => { // Read the file const fileContent = this.read() @@ -33,7 +33,7 @@ module.exports = new (class Json extends BaseVersioning { const oldVersion = objectPath.get(jsonContent, this.versionPath, null) // Get the new version - this.newVersion = bumpVersion( + this.newVersion = await bumpVersion( releaseType, oldVersion, ) diff --git a/src/version/toml.js b/src/version/toml.js index ff889ee..33ce04d 100644 --- a/src/version/toml.js +++ b/src/version/toml.js @@ -4,7 +4,7 @@ const toml = require('@iarna/toml') const BaseVersioning = require('./base') const bumpVersion = require('../helpers/bumpVersion') -module.exports = new (class Toml extends BaseVersioning{ +module.exports = new (class Toml extends BaseVersioning { /** * Bumps the version in the package.json @@ -12,14 +12,14 @@ module.exports = new (class Toml extends BaseVersioning{ * @param {!string} releaseType - The type of release * @return {*} */ - bump = (releaseType) => { + bump = async(releaseType) => { // Read the file const fileContent = this.read() const tomlContent = toml.parse(fileContent) const oldVersion = objectPath.get(tomlContent, this.versionPath, null) // Get the new version - this.newVersion = bumpVersion( + this.newVersion = await bumpVersion( releaseType, oldVersion, ) diff --git a/src/version/yaml.js b/src/version/yaml.js index c29b9b2..80975e2 100644 --- a/src/version/yaml.js +++ b/src/version/yaml.js @@ -4,7 +4,7 @@ const yaml = require('yaml') const BaseVersioning = require('./base') const bumpVersion = require('../helpers/bumpVersion') -module.exports = new (class Yaml extends BaseVersioning{ +module.exports = new (class Yaml extends BaseVersioning { /** * Bumps the version in the package.json @@ -12,14 +12,14 @@ module.exports = new (class Yaml extends BaseVersioning{ * @param {!string} releaseType - The type of release * @return {*} */ - bump = (releaseType) => { + bump = async(releaseType) => { // Read the file const fileContent = this.read() const yamlContent = yaml.parse(fileContent) || {} const oldVersion = objectPath.get(yamlContent, this.versionPath, null) // Get the new version - this.newVersion = bumpVersion( + this.newVersion = await bumpVersion( releaseType, oldVersion, ) diff --git a/test-file.json b/test-file.json new file mode 100644 index 0000000..3a6a5e0 --- /dev/null +++ b/test-file.json @@ -0,0 +1,4 @@ +{ + "name": "Test JSON", + "version": "1.4.5" +} diff --git a/test-file.toml b/test-file.toml index 5d4eb44..468a5b2 100644 --- a/test-file.toml +++ b/test-file.toml @@ -3,4 +3,4 @@ title = "test" # Comment [package] name = "test file" -version = "0.1.0" +version = "0.9.3" diff --git a/test-file.yaml b/test-file.yaml index 2b255eb..aba43fc 100644 --- a/test-file.yaml +++ b/test-file.yaml @@ -1,5 +1,5 @@ package: - version: '0.1.0' + version: '9.4.5' # Comment different: diff --git a/test-output.js b/test-output.js new file mode 100644 index 0000000..8ae7821 --- /dev/null +++ b/test-output.js @@ -0,0 +1,57 @@ +const fs = require('fs') +const assert = require('assert') +const objectPath = require('object-path') +const yaml = require('yaml') +const toml = require('@iarna/toml') + +const actionConfig = yaml.parse(fs.readFileSync('./action.yml', 'utf8')) + +const { + FILES = actionConfig.inputs['version-file'].default, + EXPECTED_VERSION_PATH = actionConfig.inputs['version-path'].default, + EXPECTED_VERSION = actionConfig.inputs['fallback-version'].default, +} = process.env + +assert.ok(FILES, 'Files not defined!') + +/** + * Test if all the files are updated + */ +FILES.split(',').map((file, index) => { + const fileContent = fs.readFileSync(file.trim(), 'utf8') + const fileExtension = file.split('.').pop() + + assert.ok(fileExtension, 'No file extension found!') + + let parsedContent = null + + switch (fileExtension.toLowerCase()) { + case 'json': + parsedContent = JSON.parse(fileContent) + break + + case 'yaml': + case 'yml': + parsedContent = yaml.parse(fileContent) + break + + case 'toml': + parsedContent = toml.parse(fileContent) + break + + default: + assert.fail('File extension not supported!') + } + + assert.ok(parsedContent, 'Content could not be parsed!') + + const newVersionInFile = objectPath.get(parsedContent, EXPECTED_VERSION_PATH, null) + + const expectedVersions = EXPECTED_VERSION.split(',') + const expectedVersion = expectedVersions.length > 0 + ? expectedVersions[index] + : expectedVersions + + assert.strictEqual(newVersionInFile, expectedVersion.trim(), 'Version does not match what is expected') +}) + diff --git a/test-pre-changelog-generation.js b/test-pre-changelog-generation.js new file mode 100644 index 0000000..0536992 --- /dev/null +++ b/test-pre-changelog-generation.js @@ -0,0 +1,29 @@ +const fs = require('fs') +const assert = require('assert') + +exports.preVersionGeneration = (version) => { + const { GITHUB_WORKSPACE } = process.env + + assert.ok(GITHUB_WORKSPACE, 'GITHUB_WORKSPACE should not be empty') + assert.ok(version, 'version should not be empty') + + const newVersion = '1.0.100' + + fs.writeFileSync('pre-changelog-generation.version.test.json', newVersion) + + return newVersion +} + +exports.preTagGeneration = (tag) => { + const { GITHUB_WORKSPACE } = process.env + + assert.ok(GITHUB_WORKSPACE, 'GITHUB_WORKSPACE should not be empty') + assert.ok(tag, 'tag should not be empty') + assert.strictEqual(tag, 'v1.0.100') + + const newTag = 'v1.0.100-alpha' + + fs.writeFileSync('pre-changelog-generation.tag.test.json', newTag) + + return newTag +} diff --git a/test/pre-commit.js b/test-pre-commit.js similarity index 59% rename from test/pre-commit.js rename to test-pre-commit.js index 15b1e7e..a828856 100644 --- a/test/pre-commit.js +++ b/test-pre-commit.js @@ -1,12 +1,12 @@ const fs = require('fs') -const t = require('assert') +const assert = require('assert') exports.preCommit = (props) => { const {GITHUB_WORKSPACE} = process.env; - t.ok(GITHUB_WORKSPACE, 'GITHUB_WORKSPACE should not be empty') - t.ok(props.tag, 'tag should not be empty') - t.ok(props.version, 'version should not be empty') + assert.ok(GITHUB_WORKSPACE, 'GITHUB_WORKSPACE should not be empty') + assert.ok(props.tag, 'tag should not be empty') + assert.ok(props.version, 'version should not be empty') const body = { workspace: GITHUB_WORKSPACE, diff --git a/test/pre-changelog-generation.js b/test/pre-changelog-generation.js deleted file mode 100644 index 762518e..0000000 --- a/test/pre-changelog-generation.js +++ /dev/null @@ -1,22 +0,0 @@ -const fs = require('fs') -const t = require('assert') - -exports.preChangelogGeneration = (props) => { - const { GITHUB_WORKSPACE } = process.env - - t.ok(GITHUB_WORKSPACE, 'GITHUB_WORKSPACE should not be empty') - t.ok(props.tag, 'tag should not be empty') - t.ok(props.version, 'version should not be empty') - - const newVersion = '1.0.100' - const newTag = 'v1.0.100' - - const body = { - version: newVersion, - tag: newTag, - } - - fs.writeFileSync('pre-changelog-generation.test.json', JSON.stringify(body, null, 2)) - - return body -}