From 14a6aaabff8512c95fb7ba4f54b4b51f1257c45a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 16 Feb 2019 15:37:48 -0800 Subject: [PATCH] Include in pip's User-Agent whether it looks like pip is in CI. --- news/5499.feature | 2 ++ src/pip/_internal/download.py | 31 +++++++++++++++++++++++++++++++ tests/unit/test_download.py | 22 +++++++++++++++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 news/5499.feature diff --git a/news/5499.feature b/news/5499.feature new file mode 100644 index 00000000000..7a786d853a9 --- /dev/null +++ b/news/5499.feature @@ -0,0 +1,2 @@ +Include in pip's User-Agent string whether it looks like pip is running +under CI. diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 2bbe1762cda..bbcc643797a 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -72,6 +72,31 @@ 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. + """ + return any(name in os.environ for name in CI_ENVIRONMENT_VARIABLES) + + def user_agent(): """ Return a string representing the user agent. @@ -135,6 +160,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), diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 532eb3fc3eb..a0aee9204b0 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -51,7 +51,27 @@ def _fake_session_get(*args, **kwargs): def test_user_agent(): - PipSession().headers["User-Agent"].startswith("pip/%s" % pip.__version__) + user_agent = PipSession().headers["User-Agent"] + + assert user_agent.startswith("pip/%s" % pip.__version__) + + +@pytest.mark.parametrize('name, expected_like_ci', [ + ('BUILD', False), + ('BUILD_BUILDID', True), + ('BUILD_ID', True), + ('CI', True), +]) +def test_user_agent__ci(monkeypatch, name, expected_like_ci): + # Clear existing names since we can be running under an actual CI. + for ci_name in ('BUILD_BUILDID', 'BUILD_ID', 'CI'): + monkeypatch.delenv(ci_name, raising=False) + + monkeypatch.setenv(name, 'true') + user_agent = PipSession().headers["User-Agent"] + + assert ('"ci":true' in user_agent) == expected_like_ci + assert ('"ci":null' in user_agent) == (not expected_like_ci) class FakeStream(object):