Skip to content

Commit

Permalink
Merge pull request #6273 from cjerdonek/issue-5499-detect-ci-for-user…
Browse files Browse the repository at this point in the history
…-agent

Fix #5499: Include in pip's User-Agent whether it looks like pip is in CI
  • Loading branch information
cjerdonek authored Feb 24, 2019
2 parents b882399 + a229f11 commit 821247d
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 3 deletions.
2 changes: 2 additions & 0 deletions news/5499.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Include in pip's User-Agent string whether it looks like pip is running
under CI.
34 changes: 34 additions & 0 deletions src/pip/_internal/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,34 @@
logger = logging.getLogger(__name__)


# These are environment variables present when running under various
# CI systems. For each variable, some CI systems that use the variable
# are indicated. The collection was chosen so that for each of a number
# of popular systems, at least one of the environment variables is used.
# This list is used to provide some indication of and lower bound for
# CI traffic to PyPI. Thus, it is okay if the list is not comprehensive.
# For more background, see: https://github.com/pypa/pip/issues/5499
CI_ENVIRONMENT_VARIABLES = (
# Azure Pipelines
'BUILD_BUILDID',
# Jenkins
'BUILD_ID',
# AppVeyor, CircleCI, Codeship, Gitlab CI, Shippable, Travis CI
'CI',
)


def looks_like_ci():
# type: () -> bool
"""
Return whether it looks like pip is running under CI.
"""
# We don't use the method of checking for a tty (e.g. using isatty())
# because some CI systems mimic a tty (e.g. Travis CI). Thus that
# method doesn't provide definitive information in either direction.
return any(name in os.environ for name in CI_ENVIRONMENT_VARIABLES)


def user_agent():
"""
Return a string representing the user agent.
Expand Down Expand Up @@ -133,6 +161,12 @@ def user_agent():
if setuptools_version is not None:
data["setuptools_version"] = setuptools_version

# Use None rather than False so as not to give the impression that
# pip knows it is not being run under CI. Rather, it is a null or
# inconclusive result. Also, we include some value rather than no
# value to make it easier to know that the check has been run.
data["ci"] = True if looks_like_ci() else None

return "{data[installer][name]}/{data[installer][version]} {json}".format(
data=data,
json=json.dumps(data, separators=(",", ":"), sort_keys=True),
Expand Down
39 changes: 36 additions & 3 deletions tests/unit/test_download.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

import pip
from pip._internal.download import (
PipSession, SafeFileCache, path_to_url, unpack_file_url, unpack_http_url,
url_to_path,
CI_ENVIRONMENT_VARIABLES, PipSession, SafeFileCache, path_to_url,
unpack_file_url, unpack_http_url, url_to_path,
)
from pip._internal.exceptions import HashMismatch
from pip._internal.models.link import Link
Expand Down Expand Up @@ -51,8 +51,41 @@ def _fake_session_get(*args, **kwargs):
rmtree(temp_dir)


def get_user_agent():
return PipSession().headers["User-Agent"]


def test_user_agent():
PipSession().headers["User-Agent"].startswith("pip/%s" % pip.__version__)
user_agent = get_user_agent()

assert user_agent.startswith("pip/%s" % pip.__version__)


@pytest.mark.parametrize('name, expected_like_ci', [
('BUILD_BUILDID', True),
('BUILD_ID', True),
('CI', True),
# Test a prefix substring of one of the variable names we use.
('BUILD', False),
])
def test_user_agent__ci(monkeypatch, name, expected_like_ci):
# Delete the variable names we use to check for CI to prevent the
# detection from always returning True in case the tests are being run
# under actual CI. It is okay to depend on CI_ENVIRONMENT_VARIABLES
# here (part of the code under test) because this setup step can only
# prevent false test failures. It can't cause a false test passage.
for ci_name in CI_ENVIRONMENT_VARIABLES:
monkeypatch.delenv(ci_name, raising=False)

# Confirm the baseline before setting the environment variable.
user_agent = get_user_agent()
assert '"ci":null' in user_agent
assert '"ci":true' not in user_agent

monkeypatch.setenv(name, 'true')
user_agent = get_user_agent()
assert ('"ci":true' in user_agent) == expected_like_ci
assert ('"ci":null' in user_agent) == (not expected_like_ci)


class FakeStream(object):
Expand Down

0 comments on commit 821247d

Please sign in to comment.