Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
31 changes: 20 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| --- | --- |
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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.

Expand Down
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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 <gleb.bahmutov@gmail.com>",
"contributors": [
"Gleb Bahmutov <gleb.bahmutov@gmail.com>",
Expand Down Expand Up @@ -32,6 +32,7 @@
},
"files": [
"src/*.js",
"src/index.d.ts",
"!src/*-spec.js"
],
"homepage": "https://github.com/currents-dev/commit-info#readme",
Expand All @@ -43,6 +44,7 @@
],
"license": "MIT",
"main": "src/",
"types": "src/index.d.ts",
"private": false,
"publishConfig": {
"registry": "https://registry.npmjs.org/"
Expand Down Expand Up @@ -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"
}
}
138 changes: 131 additions & 7 deletions src/commit-info-repos-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -94,13 +96,67 @@ 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'
})
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',
Expand All @@ -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,
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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)
})
})
})
1 change: 1 addition & 0 deletions src/git-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ module.exports = {
runGitCommand,
runGitCommandWithError,
getGitBranch,
checkIfDetached,
getSubject,
getBody,
getMessage,
Expand Down
Loading
Loading