diff --git a/.eslintrc.json b/.eslintrc.json index f03a53403..231790e39 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -37,7 +37,8 @@ "@typescript-eslint/no-non-null-asserted-optional-chain": "error", "@typescript-eslint/await-thenable": "error", "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", - "@typescript-eslint/switch-exhaustiveness-check": "error" + "@typescript-eslint/switch-exhaustiveness-check": "error", + "prettier/prettier": ["error", { "endOfLine": "auto" }] } }, { diff --git a/README.md b/README.md index 7dacb6c4d..44f37a17d 100644 --- a/README.md +++ b/README.md @@ -532,6 +532,26 @@ which means there is no limit. If set to `true`, the workflow will skip fetching branch defined with the `gh-pages-branch` variable. +#### `commit-message` (Optional) + +- Type: String +- Default: `add ${name} (${tool}) benchmark result for ${commit}` + +Overrides the message to use when committing the benchmark results to Git. + +#### `commit-user-name` (Optional) + +- Type: String +- Default: `github-action-benchmark` + +Specifies the value to use for `user.name` when committing the benchmark results to Git. + +#### `commit-user-email` (Optional) + +- Type: String +- Default: `github@users.noreply.github.com` + +Specifies the value to use for `user.email` when committing the benchmark results to Git. ### Action outputs diff --git a/action.yml b/action.yml index daa6cd343..deb697f60 100644 --- a/action.yml +++ b/action.yml @@ -79,6 +79,15 @@ inputs: max-items-in-chart: description: 'Max data points in a benchmark chart to avoid making the chart too busy. Value must be unsigned integer. No limit by default' required: false + commit-message: + description: 'Optional commit message to use for Git commits' + required: false + commit-user-name: + description: 'Optional user name to use for Git commits' + required: false + commit-user-email: + description: 'Optional email address to use for Git commits' + required: false runs: using: 'node20' diff --git a/package-lock.json b/package-lock.json index b027b8187..9ab19403f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2057,12 +2057,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3289,9 +3289,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -7625,12 +7625,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -8512,9 +8512,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" diff --git a/src/config.ts b/src/config.ts index 0ce3bd5c0..ad7ebaa6f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -25,6 +25,9 @@ export interface Config { externalDataJsonPath: string | undefined; maxItemsInChart: number | null; ref: string | undefined; + commitMessage?: string; + commitUserName?: string; + commitUserEmail?: string; } export const VALID_TOOLS = [ @@ -240,6 +243,9 @@ export async function configFromJobInput(): Promise { let externalDataJsonPath: undefined | string = core.getInput('external-data-json-path'); const maxItemsInChart = getUintInput('max-items-in-chart'); let failThreshold = getPercentageInput('fail-threshold'); + const commitMessage: string | undefined = core.getInput('commit-message') || undefined; + const commitUserName: string | undefined = core.getInput('commit-user-name') || undefined; + const commitUserEmail: string | undefined = core.getInput('commit-user-name') || undefined; validateToolType(tool); outputFilePath = await validateOutputFilePath(outputFilePath); @@ -287,5 +293,8 @@ export async function configFromJobInput(): Promise { maxItemsInChart, failThreshold, ref, + commitMessage, + commitUserName, + commitUserEmail, }; } diff --git a/src/git.ts b/src/git.ts index 1a228ac6d..ccb621dcb 100644 --- a/src/git.ts +++ b/src/git.ts @@ -11,6 +11,11 @@ interface ExecResult { code: number | null; } +interface GitConfig { + commitUserName?: string; + commitUserEmail?: string; +} + async function capture(cmd: string, args: string[]): Promise { const res: ExecResult = { stdout: '', @@ -54,15 +59,15 @@ export function getServerName(repositoryUrl: string | undefined): string { return getServerUrlObj(repositoryUrl).hostname; } -export async function cmd(additionalGitOptions: string[], ...args: string[]): Promise { +export async function cmd(config: GitConfig, additionalGitOptions: string[], ...args: string[]): Promise { core.debug(`Executing Git: ${args.join(' ')}`); const serverUrl = getServerUrl(github.context.payload.repository?.html_url); const userArgs = [ ...additionalGitOptions, '-c', - 'user.name=github-action-benchmark', + `user.name=${config.commitUserName ?? 'github-action-benchmark'}`, '-c', - 'user.email=github@users.noreply.github.com', + `user.email=${config.commitUserEmail ?? 'github@users.noreply.github.com'}`, '-c', `http.${serverUrl}/.extraheader=`, // This config is necessary to support actions/checkout@v2 (#9) ]; @@ -84,6 +89,7 @@ function getRepoRemoteUrl(token: string, repoUrl: string): string { } export async function push( + config: GitConfig, token: string, repoUrl: string | undefined, branch: string, @@ -98,10 +104,11 @@ export async function push( args = args.concat(options); } - return cmd(additionalGitOptions, ...args); + return cmd(config, additionalGitOptions, ...args); } export async function pull( + config: GitConfig, token: string | undefined, branch: string, additionalGitOptions: string[] = [], @@ -115,10 +122,11 @@ export async function pull( args = args.concat(options); } - return cmd(additionalGitOptions, ...args); + return cmd(config, additionalGitOptions, ...args); } export async function fetch( + config: GitConfig, token: string | undefined, branch: string, additionalGitOptions: string[] = [], @@ -132,10 +140,11 @@ export async function fetch( args = args.concat(options); } - return cmd(additionalGitOptions, ...args); + return cmd(config, additionalGitOptions, ...args); } export async function clone( + config: GitConfig, token: string, ghRepository: string, baseDirectory: string, @@ -150,9 +159,11 @@ export async function clone( args = args.concat(options); } - return cmd(additionalGitOptions, ...args); + return cmd(config, additionalGitOptions, ...args); } + export async function checkout( + config: GitConfig, ghRef: string, additionalGitOptions: string[] = [], ...options: string[] @@ -164,5 +175,5 @@ export async function checkout( args = args.concat(options); } - return cmd(additionalGitOptions, ...args); + return cmd(config, additionalGitOptions, ...args); } diff --git a/src/write.ts b/src/write.ts index 27e13798e..3797d0593 100644 --- a/src/write.ts +++ b/src/write.ts @@ -43,7 +43,7 @@ async function storeDataJs(dataPath: string, data: DataJson) { core.debug(`Overwrote ${dataPath} for adding new data`); } -async function addIndexHtmlIfNeeded(additionalGitArguments: string[], dir: string, baseDir: string) { +async function addIndexHtmlIfNeeded(config: Config, additionalGitArguments: string[], dir: string, baseDir: string) { const indexHtmlRelativePath = path.join(dir, 'index.html'); const indexHtmlFullPath = path.join(baseDir, indexHtmlRelativePath); try { @@ -55,7 +55,7 @@ async function addIndexHtmlIfNeeded(additionalGitArguments: string[], dir: strin } await fs.writeFile(indexHtmlFullPath, DEFAULT_INDEX_HTML, 'utf8'); - await git.cmd(additionalGitArguments, 'add', indexHtmlRelativePath); + await git.cmd(config, additionalGitArguments, 'add', indexHtmlRelativePath); console.log('Created default index.html at', indexHtmlFullPath); } @@ -389,14 +389,14 @@ async function writeBenchmarkToGitHubPagesWithRetry( if (githubToken && !skipFetchGhPages && ghRepository) { benchmarkBaseDir = './benchmark-data-repository'; - await git.clone(githubToken, ghRepository, benchmarkBaseDir); + await git.clone(config, githubToken, ghRepository, benchmarkBaseDir); rollbackActions.push(async () => { await io.rmRF(benchmarkBaseDir); }); extraGitArguments = [`--work-tree=${benchmarkBaseDir}`, `--git-dir=${benchmarkBaseDir}/.git`]; - await git.checkout(ghPagesBranch, extraGitArguments); + await git.checkout(config, ghPagesBranch, extraGitArguments); } else if (!skipFetchGhPages && (!isPrivateRepo || githubToken)) { - await git.pull(githubToken, ghPagesBranch); + await git.pull(config, githubToken, ghPagesBranch); } else if (isPrivateRepo && !skipFetchGhPages) { core.warning( "'git pull' was skipped. If you want to ensure GitHub Pages branch is up-to-date " + @@ -425,13 +425,15 @@ async function writeBenchmarkToGitHubPagesWithRetry( await storeDataJs(dataPath, data); - await git.cmd(extraGitArguments, 'add', path.join(benchmarkDataRelativeDirPath, 'data.js')); - await addIndexHtmlIfNeeded(extraGitArguments, benchmarkDataRelativeDirPath, benchmarkBaseDir); - await git.cmd(extraGitArguments, 'commit', '-m', `add ${name} (${tool}) benchmark result for ${bench.commit.id}`); + await git.cmd(config, extraGitArguments, 'add', path.join(benchmarkDataRelativeDirPath, 'data.js')); + await addIndexHtmlIfNeeded(config, extraGitArguments, benchmarkDataRelativeDirPath, benchmarkBaseDir); + + const commitMessage = config.commitMessage ?? `add ${name} (${tool}) benchmark result for ${bench.commit.id}`; + await git.cmd(config, extraGitArguments, 'commit', '-m', commitMessage); if (githubToken && autoPush) { try { - await git.push(githubToken, ghRepository, ghPagesBranch, extraGitArguments); + await git.push(config, githubToken, ghRepository, ghPagesBranch, extraGitArguments); console.log( `Automatically pushed the generated commit to ${ghPagesBranch} branch since 'auto-push' is set to true`, ); @@ -445,7 +447,7 @@ async function writeBenchmarkToGitHubPagesWithRetry( if (retry > 0) { core.debug('Rollback the auto-generated commit before retry'); - await git.cmd(extraGitArguments, 'reset', '--hard', 'HEAD~1'); + await git.cmd(config, extraGitArguments, 'reset', '--hard', 'HEAD~1'); // we need to rollback actions in order so not running them concurrently for (const action of rollbackActions) { @@ -476,16 +478,16 @@ async function writeBenchmarkToGitHubPages(bench: Benchmark, config: Config): Pr const { ghPagesBranch, skipFetchGhPages, ghRepository, githubToken } = config; if (!ghRepository) { if (!skipFetchGhPages) { - await git.fetch(githubToken, ghPagesBranch); + await git.fetch(config, githubToken, ghPagesBranch); } - await git.cmd([], 'switch', ghPagesBranch); + await git.cmd(config, [], 'switch', ghPagesBranch); } try { return await writeBenchmarkToGitHubPagesWithRetry(bench, config, 10); } finally { if (!ghRepository) { // `git switch` does not work for backing to detached head - await git.cmd([], 'checkout', '-'); + await git.cmd(config, [], 'checkout', '-'); } } } diff --git a/test/config.spec.ts b/test/config.spec.ts index 24094fd2e..16a893f98 100644 --- a/test/config.spec.ts +++ b/test/config.spec.ts @@ -348,6 +348,10 @@ describe('configFromJobInput()', function () { }); it('resolves home directory in output directory path', async function () { + if (os.platform() === 'win32') { + // Home directory is not supported on Windows + return; + } const home = os.homedir(); const absCwd = process.cwd(); if (!absCwd.startsWith(home)) { diff --git a/test/git.spec.ts b/test/git.spec.ts index 91e09b85b..744cae971 100644 --- a/test/git.spec.ts +++ b/test/git.spec.ts @@ -104,7 +104,7 @@ describe('git', function () { describe('cmd()', function () { it('runs Git command successfully', async function () { - const stdout = await cmd([], 'log', '--oneline'); + const stdout = await cmd({}, [], 'log', '--oneline'); const args = fakedExec.lastArgs; eq(stdout, 'this is test'); @@ -114,29 +114,52 @@ describe('git', function () { ok('listeners' in (args[2] as object)); }); + it('runs Git command successfully with custom config', async function () { + const config = { + commitUserName: 'some-user', + commitUserEmail: 'user@user.internal', + }; + const expectedUserArgs = [ + '-c', + 'user.name=some-user', + '-c', + 'user.email=user@user.internal', + '-c', + `http.${serverUrl}/.extraheader=`, + ]; + const stdout = await cmd(config, [], 'log', '--oneline'); + const args = fakedExec.lastArgs; + + eq(stdout, 'this is test'); + ok(args); + eq(args[0], 'git'); + eq(args[1], expectedUserArgs.concat(['log', '--oneline'])); + ok('listeners' in (args[2] as object)); + }); + it('raises an error when command returns non-zero exit code', async function () { fakedExec.exitCode = 101; - await A.rejects(() => cmd([], 'show'), /^Error: Command 'git show' failed: /); + await A.rejects(() => cmd({}, [], 'show'), /^Error: Command 'git show' failed: /); neq(fakedExec.lastArgs, null); }); it('raises an error with stderr output', async function () { fakedExec.exitCode = 101; fakedExec.stderr = 'this is error output!'; - await A.rejects(() => cmd([], 'show'), /this is error output!/); + await A.rejects(() => cmd({}, [], 'show'), /this is error output!/); }); it('raises an error when exec.exec() threw an error', async function () { fakedExec.error = 'this is error from exec.exec'; fakedExec.stderr = 'this is stderr output!'; - await A.rejects(() => cmd([], 'show'), /this is error from exec\.exec/); - await A.rejects(() => cmd([], 'show'), /this is stderr output!/); + await A.rejects(() => cmd({}, [], 'show'), /this is error from exec\.exec/); + await A.rejects(() => cmd({}, [], 'show'), /this is stderr output!/); }); }); describe('push()', function () { it('runs `git push` with given branch and options', async function () { - const stdout = await push('this-is-token', undefined, 'my-branch', [], 'opt1', 'opt2'); + const stdout = await push({}, 'this-is-token', undefined, 'my-branch', [], 'opt1', 'opt2'); const args = fakedExec.lastArgs; eq(stdout, 'this is test'); @@ -158,7 +181,7 @@ describe('git', function () { describe('pull()', function () { it('runs `git pull` with given branch and options with token', async function () { - const stdout = await pull('this-is-token', 'my-branch', [], 'opt1', 'opt2'); + const stdout = await pull({}, 'this-is-token', 'my-branch', [], 'opt1', 'opt2'); const args = fakedExec.lastArgs; eq(stdout, 'this is test'); @@ -177,7 +200,7 @@ describe('git', function () { }); it('runs `git pull` with given branch and options without token', async function () { - const stdout = await pull(undefined, 'my-branch', [], 'opt1', 'opt2'); + const stdout = await pull({}, undefined, 'my-branch', [], 'opt1', 'opt2'); const args = fakedExec.lastArgs; eq(stdout, 'this is test'); @@ -189,7 +212,7 @@ describe('git', function () { describe('fetch()', function () { it('runs `git fetch` with given branch and options with token', async function () { - const stdout = await fetch('this-is-token', 'my-branch', [], 'opt1', 'opt2'); + const stdout = await fetch({}, 'this-is-token', 'my-branch', [], 'opt1', 'opt2'); const args = fakedExec.lastArgs; eq(stdout, 'this is test'); @@ -208,7 +231,7 @@ describe('git', function () { }); it('runs `git fetch` with given branch and options without token', async function () { - const stdout = await fetch(undefined, 'my-branch', [], 'opt1', 'opt2'); + const stdout = await fetch({}, undefined, 'my-branch', [], 'opt1', 'opt2'); const args = fakedExec.lastArgs; eq(stdout, 'this is test'); diff --git a/test/write.spec.ts b/test/write.spec.ts index e88543cbb..cd7772e1a 100644 --- a/test/write.spec.ts +++ b/test/write.spec.ts @@ -34,7 +34,7 @@ class GitSpy { } call(func: GitFunc, args: unknown[]) { - this.history.push([func, args]); + this.history.push([func, args.slice(1)]); } clear() {