diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9c3fa74 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +## 2.0.0 + +Breaking: `commitInfo()` returns other values than 1.x. + +- Fills the fields git could not read from the CI provider's variables: GitHub Actions, GitLab, CircleCI, Jenkins, Azure Pipelines, Bitbucket Pipelines, Buildkite, AWS CodeBuild, and the providers listed in `src/ci.js`. Priority: `COMMIT_INFO_*`, then git, then the CI provider. The variables are the ones the Currents Playwright reporter used. +- `remote` comes from `COMMIT_INFO_REMOTE`, then the CI provider's variables, then git. The CI providers that set a remote are AWS CodeBuild, Azure Pipelines, Bamboo, Buildkite, CircleCI, Drone, GitLab, Semaphore and Netlify. This is the rule the Currents Playwright reporter used. +- On Semaphore, `remote` is the clone URL in `SEMAPHORE_GIT_URL`. The Currents Playwright reporter reported `SEMAPHORE_GIT_REPO_SLUG` (`owner/repo`), which Currents could not build commit links from. +- On Bamboo, `remote` comes from `bamboo_planRepository_repositoryUrl`. The Currents Playwright reporter read `bamboo_planRepository_repositoryURL`, which Bamboo does not set. +- `remote` has no user name or password, also in the `DEBUG=commit-info` output and from `getRemoteOrigin()` and `getCiCommitInfo()`. +- On CI, when git refuses a repository owned by another user ("dubious ownership"), the read-only git commands run again with `-c safe.directory=*`. CI means that `CI` is set or a CI provider is detected, so Jenkins counts. `GOOGLE_CLOUD_PROJECT`, `GCP_PROJECT`, `GCLOUD_PROJECT` and `JENKINS_HOME` alone do not count. +- Prints one warning when git fails and fields stay empty, also when git is not in `PATH`. +- New exports: `getCiCommitInfo`, `detectCiProvider`, `removeCredentials`, and TypeScript types. + +Migration: + +- If your code fills empty `commitInfo()` fields from CI provider variables, remove it: `commitInfo()` does that now. +- If your code replaces the git remote with the CI provider's remote, remove it: `commitInfo()` returns `COMMIT_INFO_REMOTE`, else the CI provider's remote, else the git remote, without credentials. diff --git a/README.md b/README.md index 81b568b..d3e9d4e 100644 --- a/README.md +++ b/README.md @@ -26,21 +26,30 @@ commitInfo(folder) // sha // timestamp (in seconds since epoch) // remote (without credentials) + // ghaEventData (GitHub Actions pull request events only) }) ``` +Each property comes from the first of these that has a value, or is `null`: + +1. the `COMMIT_INFO_*` environment variable, see [Fallback environment variables](#fallback-environment-variables) +2. git, see [src/git-api.js](src/git-api.js) +3. the CI provider's environment variables, see [CI provider variables](#ci-provider-variables) + +The `remote` is the exception: the CI provider's value comes before git. `COMMIT_INFO_REMOTE` still comes first. On Azure Pipelines, for example, the clone often has an SSH remote, and `BUILD_REPOSITORY_URI` has the HTTPS URL. The CI providers that set a remote are AWS CodeBuild, Azure Pipelines, Bamboo, Buildkite, CircleCI, Drone, GitLab, Semaphore and Netlify, see [src/ci.js](src/ci.js). + Notes: -- Code assumes there is `.git` folder and uses Git commands to get each property, like `git show -s --pretty=%B`, see [src/git-api.js](src/git-api.js). Note: there is fallback to environment variables. -- Resolves with [Bluebird](https://github.com/petkaantonov/bluebird) promise. -- Only uses Git commands, see [src/git-api.js](src/git-api.js) -- If a command fails, returns `null` for each property -- `remote` never contains a user name or password, also when it comes from `COMMIT_INFO_REMOTE`. +- git reports no branch for a detached checkout (`HEAD`), so the branch comes from the CI provider. Branch values from `COMMIT_INFO_BRANCH` and the CI provider are reported as they are. +- `remote` never contains a user name or password, wherever it came from. GitLab's `CI_REPOSITORY_URL`, for example, holds a job token. +- Resolves with a [Bluebird](https://github.com/petkaantonov/bluebird) promise. - If you need to debug, run with `DEBUG=commit-info` environment variable. The debug output does not contain the remote's credentials. +- When git fails and fields are still empty after the CI provider's variables, `commitInfo` prints one warning with the git error and the `COMMIT_INFO_*` variables to set. No warning when there is no repository and the CI provider's variables fill the fields, or when there is no repository and no CI provider. +- When git is not installed or not in `PATH`, all the git values are empty and the warning says that git was not found in `PATH`. Install git or set the `COMMIT_INFO_*` variables. ## CI provider variables -`getCiCommitInfo()` reads the commit from the variables of the CI provider the process runs on. The branch comes from: +When git does not return a value, it comes from the variables of the CI provider the process runs on. The remote comes from these variables first. The branch comes from: | Provider | Branch | | --- | --- | @@ -52,13 +61,13 @@ Notes: | Bitbucket Pipelines | `BITBUCKET_BRANCH` | | Buildkite | `BUILDKITE_BRANCH` | -AWS CodeBuild gives no branch. The other properties and providers are in [src/ci.js](src/ci.js). +AWS CodeBuild gives no branch. The other properties and providers are in [src/ci.js](src/ci.js). `getCiCommitInfo()` returns these values and the provider name. ## Containers git 2.35.2 and later refuse to read a repository owned by another user, with `fatal: unsafe repository` (2.35.2 to 2.37.x) or `fatal: detected dubious ownership in repository` (2.38.0 and later). This is common when a container runs as root on a checkout made by another user. On CI, the read-only git commands then run again with `-c safe.directory=*`. CI means that the `CI` variable is set to a value other than `false` or `0`, or that one of the CI providers in [src/ci-provider.js](src/ci-provider.js) is detected; Jenkins, for example, does not set `CI`. `GOOGLE_CLOUD_PROJECT`, `GCP_PROJECT`, `GCLOUD_PROJECT` and `JENKINS_HOME` do not count, because developers often have them set in their shell. `*` because the folder can be a subfolder of the repository. -git 2.35.2 to 2.37.x ignore `safe.directory` on the command line, so there the git values are `null` (git 2.38.0 and later respect it); run `git config --global --add safe.directory '*'` in the container or set the `COMMIT_INFO_*` variables. +git 2.35.2 to 2.37.x ignore `safe.directory` on the command line, so there `commitInfo` prints the warning (git 2.38.0 and later respect it); run `git config --global --add safe.directory '*'` in the container or set the `COMMIT_INFO_*` variables. ## Pull request builds @@ -80,7 +89,7 @@ The `COMMIT_INFO_*` variables below still take priority. When `COMMIT_INFO_SHA` ## Fallback environment variables -If getting the commit information using `git` fails for some reason, you can provide the commit information by setting the environment variables. This module will look at the following environment variables as a fallback +You can provide the commit information by setting these environment variables. They take priority over git and the CI provider's variables. ``` branch: COMMIT_INFO_BRANCH @@ -108,11 +117,11 @@ See [docker-example](docker-example) for a full example. ## Individual methods In addition to `commitInfo` this module also exposes individual promise-returning -methods `getBranch`, `getMessage`, `getEmail`, `getAuthor`, `getSha`, `getTimestamp`, `getRemoteOrigin`. These methods do NOT use fallback environment variables. `getRemoteOrigin` returns the remote without credentials. +methods `getBranch`, `getMessage`, `getEmail`, `getAuthor`, `getSha`, `getTimestamp`, `getRemoteOrigin`. These methods use git only, not the environment variables. `getRemoteOrigin` returns the remote without credentials. Other exports: -- `getCiCommitInfo(env = process.env)`: the CI provider's values and the provider name, see [CI provider variables](#ci-provider-variables). The `remote` has no credentials. +- `getCiCommitInfo(env = process.env)`: the CI provider's values, see [CI provider variables](#ci-provider-variables). The `remote` has no credentials. - `detectCiProvider(env = process.env)`: the CI provider name, such as `githubActions`, or `null`. - `removeCredentials(url)`: removes the user name and password from a URL, keeping the port. Returns other values, such as `git@github.com:o/r.git`, as they are. diff --git a/package-lock.json b/package-lock.json index 2e71245..5026058 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,19 @@ { "name": "@currents/commit-info", - "version": "1.1.0", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@currents/commit-info", - "version": "1.1.0", + "version": "2.0.0", "license": "MIT", "dependencies": { "bluebird": "3.5.5", "check-more-types": "2.24.0", "debug": "4.3.4", "execa": "1.0.0", - "lazy-ass": "1.6.0", - "ramda": "0.26.1" + "lazy-ass": "1.6.0" }, "devDependencies": { "ban-sensitive-files": "1.9.2", @@ -14317,7 +14316,8 @@ "node_modules/ramda": { "version": "0.26.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", - "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==" + "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==", + "dev": true }, "node_modules/rc": { "version": "1.2.8", @@ -29952,7 +29952,8 @@ "ramda": { "version": "0.26.1", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.26.1.tgz", - "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==" + "integrity": "sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==", + "dev": true }, "rc": { "version": "1.2.8", diff --git a/package.json b/package.json index a08daeb..05a7fcc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@currents/commit-info", "description": "Collects Git commit info from CI or from CLI", - "version": "1.1.0", + "version": "2.0.0", "author": "Gleb Bahmutov ", "contributors": [ "Gleb Bahmutov ", @@ -32,6 +32,7 @@ }, "files": [ "src/*.js", + "src/index.d.ts", "!src/*-spec.js" ], "homepage": "https://github.com/currents-dev/commit-info#readme", @@ -43,6 +44,7 @@ ], "license": "MIT", "main": "src/", + "types": "src/index.d.ts", "private": false, "publishConfig": { "registry": "https://registry.npmjs.org/" @@ -99,7 +101,6 @@ "check-more-types": "2.24.0", "debug": "4.3.4", "execa": "1.0.0", - "lazy-ass": "1.6.0", - "ramda": "0.26.1" + "lazy-ass": "1.6.0" } } diff --git a/src/commit-info-repos-spec.js b/src/commit-info-repos-spec.js index 447c4e7..ff3f604 100644 --- a/src/commit-info-repos-spec.js +++ b/src/commit-info-repos-spec.js @@ -28,11 +28,13 @@ const git = (cwd, ...args) => /** * Runs commitInfo in a new process, so the CI variables of the machine that - * runs the tests do not apply. + * runs the tests do not apply and each test can get its warning. * Resolves with the result and everything the process printed. */ -function runCommitInfo (cwd, env) { - const script = `require(${JSON.stringify(__dirname)}).commitInfo() +function runCommitInfo (cwd, env, folder) { + const script = `require(${JSON.stringify(__dirname)}).commitInfo(${ + folder ? JSON.stringify(folder) : '' + }) .then(info => console.log(JSON.stringify(info)))` const child = spawnSync(process.execPath, ['-e', script], { cwd, @@ -94,6 +96,28 @@ describe('commitInfo in real repositories', function () { assert.strictEqual(stderr, '') }) + it('takes the branch from CI for a detached checkout', () => { + git(repo, 'checkout', '-q', '--detach') + const { info } = runCommitInfo(repo, { + GITHUB_ACTIONS: 'true', + GITHUB_REF: 'refs/pull/12/merge', + GITHUB_REF_NAME: '12/merge', + GITHUB_HEAD_REF: 'feature/x', + GITHUB_SHA: 'ci-sha' + }) + assert.strictEqual(info.branch, 'feature/x') + assert.strictEqual(info.sha, sha) + }) + + it('prefers the git branch to the CI branch', () => { + const { info } = runCommitInfo(repo, { + TF_BUILD: 'True', + AZURE_HTTP_USER_AGENT: 'agent', + BUILD_SOURCEBRANCH: 'refs/heads/other' + }) + assert.strictEqual(info.branch, 'main') + }) + it('prefers COMMIT_INFO_BRANCH to git and keeps it as it is', () => { const { info } = runCommitInfo(repo, { COMMIT_INFO_BRANCH: 'refs/heads/from-env' @@ -101,6 +125,38 @@ describe('commitInfo in real repositories', function () { assert.strictEqual(info.branch, 'refs/heads/from-env') }) + describe('remote', () => { + const azureEnv = { + TF_BUILD: 'True', + AZURE_HTTP_USER_AGENT: 'agent', + BUILD_REPOSITORY_URI: 'https://org@dev.azure.com/org/p/_git/repo' + } + + it('comes from the CI provider before git', () => { + const { info } = runCommitInfo(repo, azureEnv) + assert.strictEqual(info.remote, 'https://dev.azure.com/org/p/_git/repo') + }) + + it('comes from git when the CI provider has no remote', () => { + const { info } = runCommitInfo(repo, { + GITHUB_ACTIONS: 'true', + GITHUB_REF: 'refs/heads/main' + }) + assert.strictEqual(info.remote, 'https://gitlab.com/org/repo.git') + }) + + it('comes from COMMIT_INFO_REMOTE before the CI provider', () => { + const { info } = runCommitInfo( + repo, + Object.assign( + { COMMIT_INFO_REMOTE: 'git@github.com:o/r.git' }, + azureEnv + ) + ) + assert.strictEqual(info.remote, 'git@github.com:o/r.git') + }) + }) + describe('credentials', () => { const gitlabEnv = { GITLAB_CI: 'true', @@ -117,6 +173,12 @@ describe('commitInfo in real repositories', function () { assert(!output.includes(TOKEN), output) }) + it('are removed from the CI remote when there is no repository', () => { + const { info, output } = runCommitInfo(root, gitlabEnv) + assert.strictEqual(info.remote, 'https://gitlab.com/org/repo.git') + assert(!output.includes(TOKEN), output) + }) + it('are removed from COMMIT_INFO_REMOTE', () => { const { info, output } = runCommitInfo( repo, @@ -136,11 +198,70 @@ describe('commitInfo in real repositories', function () { assert.strictEqual(stderr, '') }) - it('has no values when git is not in PATH', () => { - const { info } = runCommitInfo(repo, { PATH: root }) + it('warns once when git is not in PATH', () => { + const { info, stderr } = runCommitInfo(repo, { PATH: root }) assert.strictEqual(info.sha, null) assert.strictEqual(info.branch, null) assert.strictEqual(info.remote, null) + assert(stderr.includes('git was not found in PATH'), stderr) + assert.strictEqual(stderr.match(/\[commit-info\]/g).length, 1, stderr) + }) + + describe('a folder that does not exist', () => { + it('says so on CI', () => { + const missing = path.join(root, 'missing') + const { info, stderr } = runCommitInfo(root, { CI: 'true' }, missing) + assert.strictEqual(info.sha, null) + assert(stderr.includes(`${missing} does not exist`), stderr) + assert(!stderr.includes('git was not found'), stderr) + }) + + it('does not warn outside CI', () => { + const missing = path.join(root, 'missing') + const { stderr } = runCommitInfo(root, {}, missing) + assert.strictEqual(stderr, '') + }) + }) + + describe('no repository', () => { + it('warns when CI variables do not fill the fields', () => { + const { info, stderr } = runCommitInfo(root, { + GITHUB_ACTIONS: 'true', + GITHUB_REF: 'refs/heads/main', + GITHUB_SHA: sha + }) + assert.strictEqual(info.branch, 'main') + assert.strictEqual(info.sha, sha) + assert.strictEqual(info.message, null) + assert(/git failed in .*not a git repository/i.test(stderr), stderr) + assert( + stderr.includes( + 'Set COMMIT_INFO_MESSAGE, COMMIT_INFO_AUTHOR, COMMIT_INFO_EMAIL' + ), + stderr + ) + assert.strictEqual(stderr.match(/git failed/g).length, 1, stderr) + }) + + it('does not warn outside CI', () => { + const { info, stderr } = runCommitInfo(root, {}) + assert.strictEqual(info.sha, null) + assert.strictEqual(stderr, '') + }) + + it('does not warn when CI variables fill the fields', () => { + const { info, stderr } = runCommitInfo(root, { + BUILDKITE: 'true', + BUILDKITE_BRANCH: 'main', + BUILDKITE_COMMIT: sha, + BUILDKITE_MESSAGE: 'feat: first', + BUILDKITE_BUILD_CREATOR: 'Jane', + BUILDKITE_BUILD_CREATOR_EMAIL: 'jane@example.com' + }) + assert.strictEqual(info.sha, sha) + assert.strictEqual(info.author, 'Jane') + assert.strictEqual(stderr, '') + }) }) describe('repository owned by another user', () => { @@ -173,9 +294,12 @@ describe('commitInfo in real repositories', function () { assert.strictEqual(stderr, '') }) - it('is not read outside CI', () => { - const { info } = runCommitInfo(repo, otherOwner) + it('is not read outside CI, with a warning', () => { + const { info, stderr } = runCommitInfo(repo, otherOwner) assert.strictEqual(info.sha, null) + assert(stderr.includes('dubious ownership'), stderr) + assert(stderr.includes('safe.directory'), stderr) + assert(stderr.includes('COMMIT_INFO_SHA'), stderr) }) }) }) diff --git a/src/git-api.js b/src/git-api.js index 62a879d..c7daeda 100644 --- a/src/git-api.js +++ b/src/git-api.js @@ -149,6 +149,7 @@ module.exports = { runGitCommand, runGitCommandWithError, getGitBranch, + checkIfDetached, getSubject, getBody, getMessage, diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000..c7d3c31 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,57 @@ +/** Copied from the GitHub event payload; a field the payload lacks is undefined */ +export interface GhaEventData { + headRef?: string; + headSha?: string; + baseRef?: string; + baseSha?: string; + issueUrl?: string; + htmlUrl?: string; + prTitle?: string; + senderAvatarUrl?: string; + senderHtmlUrl?: string; +} + +export interface CommitInfo { + branch: string | null; + message: string | null; + email: string | null; + author: string | null; + sha: string | null; + /** seconds since epoch */ + timestamp: string | null; + /** without credentials */ + remote: string | null; + ghaEventData?: GhaEventData; +} + +export interface CiCommitInfo { + provider: string | null; + branch: string | null; + message: string | null; + email: string | null; + author: string | null; + sha: string | null; + /** without credentials */ + remote: string | null; + /** no CI provider sets it */ + timestamp: null; +} + +type Env = Record; + +export function commitInfo(folder?: string): Promise; +export function getCiCommitInfo(env?: Env): CiCommitInfo; +export function detectCiProvider(env?: Env): string | null; +export function removeCredentials( + url: T +): T extends string ? string : T; + +export function getBranch(folder?: string): Promise; +export function getMessage(folder?: string): Promise; +export function getEmail(folder?: string): Promise; +export function getAuthor(folder?: string): Promise; +export function getSha(folder?: string): Promise; +export function getRemoteOrigin(folder?: string): Promise; +export function getSubject(folder?: string): Promise; +export function getTimestamp(folder?: string): Promise; +export function getBody(folder?: string): Promise; diff --git a/src/index.js b/src/index.js index 9c1ac0b..35d3adf 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ 'use strict' const debug = require('debug')('commit-info') +const fs = require('fs') const { getSubject, getBody, @@ -9,51 +10,172 @@ const { getAuthor, getSha, getTimestamp, - getRemoteOrigin + getRemoteOrigin, + gitCommands, + runGitCommandWithError, + readRemoteOrigin, + checkIfDetached } = require('./git-api') const { getBranch, getCommitInfoFromEnvironment, + getEnvName, + getFields, getGhaEventData } = require('./utils') const { getPullRequestHeadCommit } = require('./pull-request-head') const { getCiCommitInfo, detectCiProvider } = require('./ci') const { removeCredentials } = require('./remove-credentials') +const { describeGitError, isCi, isDubiousOwnership } = require('./run-git') const Promise = require('bluebird') -const { mergeWith, or } = require('ramda') +const GIT_COMMANDS = { + branch: gitCommands.branch, + message: gitCommands.message, + email: gitCommands.email, + author: gitCommands.author, + sha: gitCommands.sha, + timestamp: gitCommands.timestamp +} + +// No CI provider sets a timestamp, and many repositories have no remote, so +// missing values there do not cause a warning +const WARN_FIELDS = ['branch', 'sha', 'message', 'author', 'email'] + +const withoutRemoteCredentials = info => + Object.assign({}, info, { remote: removeCredentials(info.remote) }) + +/** + * Resolves with `{ info, error }`: the values git returned, and the first + * error git failed with. + */ +function readGit (folder) { + const reads = { remote: readRemoteOrigin(folder) } + Object.keys(GIT_COMMANDS).forEach(field => { + reads[field] = runGitCommandWithError(GIT_COMMANDS[field], folder) + }) + return Promise.props(reads).then(results => { + const info = {} + let error = null + getFields().forEach(field => { + info[field] = results[field].value + error = error || results[field].error + }) + info.branch = checkIfDetached(info.branch) + return { info, error } + }) +} + +/** + * For each field the first value that is set wins: + * 1. the COMMIT_INFO_* variable + * 2. git + * 3. the CI provider's variables + * + * For the remote the CI provider's value wins over git, as in the Currents + * Playwright reporter: an Azure Pipelines clone often has an SSH remote, and + * the pull request link needs the HTTPS URL in BUILD_REPOSITORY_URI. + */ +function combineCommitInfo (fromEnvironment, fromGit, fromCi) { + const combined = {} + getFields().forEach(field => { + combined[field] = + fromEnvironment[field] || fromGit[field] || fromCi[field] || null + }) + combined.remote = + fromEnvironment.remote || fromCi.remote || fromGit.remote || null + return combined +} + +// Playwright workers each call commitInfo; one warning per process is enough +let warned = false + +const isNotRepository = error => + /not a git repository/i.test(String(error.stderr || error.message || '')) + +// Node reports a folder that does not exist as `spawn git ENOENT` too +const isGitMissing = error => error.code === 'ENOENT' + +function describeFailure (folder, gitError) { + if (!fs.existsSync(folder)) { + return `[commit-info] ${folder} does not exist.` + } + if (isGitMissing(gitError)) { + return `[commit-info] git was not found in PATH, so the commit in ${folder} could not be read.` + } + return `[commit-info] git failed in ${folder}: ${describeGitError(gitError)}` +} + +function warnAboutMissingFields (folder, gitError, info) { + const missing = WARN_FIELDS.filter(field => !info[field]) + if (!gitError || !missing.length || warned) { + return + } + // a command run outside a repository on a developer machine has no commit + const noRepository = isNotRepository(gitError) || !fs.existsSync(folder) + if (noRepository && !isCi()) { + return + } + warned = true + const lines = [ + describeFailure(folder, gitError), + `Missing commit fields: ${missing.join(', ')}. Set ${missing + .map(getEnvName) + .join(', ')} to provide them.` + ] + if (isDubiousOwnership(gitError)) { + lines.push( + "Or allow the repository: git config --global --add safe.directory '*'. " + + 'git 2.35.2 to 2.37.x ignore safe.directory set on the command line.' + ) + } + console.warn(lines.join('\n')) +} + +/** + * Resolves with the commit the folder has checked out. The COMMIT_INFO_* + * variables take priority over git; the CI provider's variables fill the + * fields git could not read, and take priority over git for the remote. The + * remote has no credentials. + * + * @param {string} [folder] defaults to the current working directory + */ function commitInfo (folder) { folder = folder || process.cwd() debug('commit-info in folder', folder) return Promise.props({ - branch: getBranch(folder), - message: getMessage(folder), - email: getEmail(folder), - author: getAuthor(folder), - sha: getSha(folder), - timestamp: getTimestamp(folder), - remote: getRemoteOrigin(folder), + git: readGit(folder), ghaEventData: getGhaEventData( process.env.GITHUB_EVENT_PATH, process.env.GITHUB_ACTIONS ) }) - .then(info => { + .then(({ git, ghaEventData }) => { // COMMIT_INFO_SHA names the commit to report, so it is used as is if (process.env.COMMIT_INFO_SHA) { - return info + return Object.assign({ ghaEventData }, git) } - return getPullRequestHeadCommit(folder, info.sha, info.ghaEventData).then( - head => Object.assign({}, info, head) + return getPullRequestHeadCommit(folder, git.info.sha, ghaEventData).then( + head => ({ + info: Object.assign({}, git.info, head), + error: git.error, + ghaEventData + }) ) }) - .then(info => { - const envVariables = getCommitInfoFromEnvironment() - envVariables.remote = removeCredentials(envVariables.remote) - debug('git commit: %o', info) + .then(({ info: gitInfo, error: gitError, ghaEventData }) => { + const envVariables = withoutRemoteCredentials( + getCommitInfoFromEnvironment() + ) + const ciInfo = getCiCommitInfo() + debug('git commit: %o', gitInfo) debug('env commit: %o', envVariables) - return mergeWith(or, envVariables, info) + debug('CI commit: %o', ciInfo) + + const info = combineCommitInfo(envVariables, gitInfo, ciInfo) + warnAboutMissingFields(folder, gitError, info) + return Object.assign(info, { ghaEventData }) }) } diff --git a/src/utils-spec.js b/src/utils-spec.js index d4f1422..026ba65 100644 --- a/src/utils-spec.js +++ b/src/utils-spec.js @@ -2,7 +2,6 @@ const la = require('lazy-ass') const is = require('check-more-types') -const { mergeWith, or } = require('ramda') const sinon = require('sinon') const fs = require('fs') @@ -61,13 +60,6 @@ describe('utils', () => { }) }) - describe('R.mergeWith', () => { - it('keeps non-empty string', () => { - const o = mergeWith(or, { foo: 'foo' }, { foo: '' }) - la(o.foo === 'foo', o) - }) - }) - describe('firstFoundValue', () => { const { firstFoundValue } = require('./utils') diff --git a/src/utils.js b/src/utils.js index ad0e1a7..745668e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -29,25 +29,26 @@ function getBranch (pathToRepo) { } /** - * Looks up commit information from environment keys. + * Returns list of Git properties that this module searches for */ -function getCommitInfoFromEnvironment (env = process.env) { - return { - branch: getValue('COMMIT_INFO_BRANCH')(env), - message: getValue('COMMIT_INFO_MESSAGE')(env), - email: getValue('COMMIT_INFO_EMAIL')(env), - author: getValue('COMMIT_INFO_AUTHOR')(env), - sha: getValue('COMMIT_INFO_SHA')(env), - timestamp: getValue('COMMIT_INFO_TIMESTAMP')(env), - remote: getValue('COMMIT_INFO_REMOTE')(env) - } +function getFields () { + return ['branch', 'message', 'email', 'author', 'sha', 'remote', 'timestamp'] } /** - * Returns list of Git properties that this module searches for + * Name of the environment variable that sets a field, as in COMMIT_INFO_SHA */ -function getFields () { - return ['branch', 'message', 'email', 'author', 'sha', 'remote', 'timestamp'] +const getEnvName = field => `COMMIT_INFO_${field.toUpperCase()}` + +/** + * Looks up commit information from environment keys. + */ +function getCommitInfoFromEnvironment (env = process.env) { + const info = {} + getFields().forEach(field => { + info[field] = getValue(getEnvName(field))(env) + }) + return info } /** @@ -85,6 +86,7 @@ module.exports = { firstFoundValue, getBranch, getCommitInfoFromEnvironment, + getEnvName, getFields, getGhaEventData }