├── .flake8 ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.rst ├── MANIFEST.in ├── README.rst ├── devpi_jenkins ├── __init__.py ├── devpibootstrap.py.template └── main.py ├── jenkins1.png ├── jenkins2.png ├── jenkins3.png ├── setup.py ├── tests ├── conftest.py ├── test_devpibootstrap.py └── test_jenkins.py └── tox.ini /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,E741,W504 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "CI" 3 | 4 | on: 5 | push: 6 | 7 | jobs: 8 | tests: 9 | name: "${{ matrix.tox-envs }} - Python ${{ matrix.python-version }}" 10 | runs-on: "ubuntu-latest" 11 | env: 12 | PY_COLORS: 1 13 | 14 | strategy: 15 | matrix: 16 | include: 17 | - python-version: "3.8" 18 | tox-envs: "py38" 19 | - python-version: "3.12" 20 | tox-envs: "py312" 21 | - python-version: "3.8" 22 | tox-envs: "py38-server520" 23 | - python-version: "3.12" 24 | tox-envs: "py312-server520" 25 | 26 | steps: 27 | - uses: "actions/checkout@v4" 28 | - uses: "actions/setup-python@v5" 29 | with: 30 | python-version: "${{ matrix.python-version }}" 31 | - name: "Install dependencies" 32 | run: | 33 | set -xe -o nounset 34 | python -VV 35 | python -m site 36 | python -m pip install --upgrade pip setuptools wheel 37 | python -m pip install --upgrade virtualenv tox 38 | 39 | - name: "Run tox targets for ${{ matrix.python-version }}" 40 | run: | 41 | set -xe -o nounset 42 | python -m tox -a -vv 43 | python -m tox -v -e ${{ matrix.tox-envs }} -- -v --color=yes 44 | 45 | flake8: 46 | env: 47 | PY_COLORS: 1 48 | 49 | runs-on: "ubuntu-latest" 50 | 51 | steps: 52 | - uses: "actions/checkout@v4" 53 | - uses: "actions/setup-python@v5" 54 | with: 55 | python-version: "3.x" 56 | - name: "Install dependencies" 57 | shell: "bash" 58 | run: | 59 | set -xe -o nounset 60 | python -VV 61 | python -m site 62 | python -m pip install --upgrade pip flake8 setuptools wheel 63 | - name: "Run flake8" 64 | shell: "bash" 65 | run: | 66 | set -xe -o nounset 67 | flake8 --config .flake8 devpi_jenkins tests 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.sublime-* 3 | .cache/ 4 | /.coverage 5 | /.tox/ 6 | /bin/ 7 | /dist/ 8 | /htmlcov/ 9 | /include/ 10 | /lib/ 11 | /pip-selfcheck.json 12 | /pyvenv.cfg 13 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 3.0.1 - 2024-08-04 5 | ------------------ 6 | 7 | - Replace pkg_resources with importlib.resources. 8 | [fschulze] 9 | 10 | - Replace unmaintained py library usage with builtin Python functionality. 11 | [fschulze] 12 | 13 | - Drop support for Python <= 3.7. 14 | [fschulze] 15 | 16 | - Add support for Python 3.12. 17 | 18 | 19 | 3.0.0 - 2023-12-19 20 | ------------------ 21 | 22 | - Drop support for Python <= 3.6. 23 | [fschulze] 24 | 25 | - Fix for new pluggy version. 26 | [fschulze] 27 | 28 | - Remove unused import from ``devpibootstrap.py.template``. 29 | [fschulze] 30 | 31 | 32 | 2.0.0 - 2016-04-25 33 | ------------------ 34 | 35 | - Drop support for Python 2.6 36 | [fschulze] 37 | 38 | - fixes for devpi-server 3.0.0, older versions aren't supported anymore 39 | [fschulze] 40 | 41 | 42 | 1.0.0 - 2015-05-13 43 | ------------------ 44 | 45 | - include version in testspec for ``devpi test`` command 46 | [fschulze] 47 | 48 | - separated into plugin from devpi-server 49 | [fschulze (Florian Schulze)] 50 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.ini *.rst 2 | include devpi_jenkins/*.template 3 | recursive-include tests *.py 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | devpi-jenkins: Jenkins build trigger for devpi-server 2 | ===================================================== 3 | 4 | For use with devpi-server >= 2.2.0. 5 | 6 | Installation 7 | ------------ 8 | 9 | ``devpi-jenkins`` needs to be installed alongside ``devpi-server``. 10 | 11 | You can install it with:: 12 | 13 | pip install devpi-jenkins 14 | 15 | For ``devpi-server`` there is no configuration needed, as it will automatically discover the plugin through calling hooks using the setuptools entry points mechanism. 16 | 17 | Details about configuration below. 18 | 19 | Configuration 20 | ------------- 21 | 22 | devpi-jenkins can trigger Jenkins to test uploaded packages using tox_. 23 | This needs configuration on two sides: 24 | 25 | - devpi: configuring an index to send POST requests to Jenkins upon upload 26 | 27 | - Jenkins: adding one or more jobs which can get triggered by devpi-jenkins. 28 | 29 | .. _tox: https://tox.readthedocs.io/ 30 | 31 | Configuring a devpi index to trigger Jenkins 32 | ++++++++++++++++++++++++++++++++++++++++++++ 33 | 34 | Here is a example command, using a ``/testuser/dev`` index 35 | and a Jenkins server at http://localhost:8080:: 36 | 37 | # needs one Jenkins job for each name of uploaded packages 38 | devpi index /testuser/dev uploadtrigger_jenkins=http://localhost:8080/job/{pkgname}/build 39 | 40 | Any package which gets uploaded to ``/testuser/dev`` will now trigger 41 | a POST request to the specified url. The ``{pkgname}`` and 42 | ``{pkgversion}`` strings will be substituted with the name of the 43 | uploaded package. You don't need to specify such substitutions, 44 | however, if you rather want to have one generic Jenkins job which 45 | executes all tests for all your uploads:: 46 | 47 | # one generic job for all uploaded packages 48 | devpi index /testuser/dev uploadtrigger_jenkins=http://localhost:8080/job/multijob/build 49 | 50 | This requires a single ``multijob`` on the Jenkins side whereas the prior 51 | configuration would require a job for each package name that you possibly 52 | upload. 53 | 54 | Note that uploading a package will succeed independently if a build job could 55 | be submitted successfully to Jenkins. 56 | 57 | Configuring Jenkins job(s) 58 | ++++++++++++++++++++++++++ 59 | 60 | On the Jenkins side, you need to configure one or more jobs which can 61 | be triggered by devpi-jenkins. Each job is configured in the same way: 62 | 63 | - go to main Jenkins screen 64 | 65 | - hit "New Job" and enter a name ("multijob" if you want to configure 66 | a generic job), then select "freey style software project", hit OK. 67 | 68 | .. image:: jenkins1.png 69 | :align: center 70 | 71 | - enable "This build is parametrized" and add a "File Parameter", 72 | setting the file location to ``jobscript.py``. 73 | 74 | .. image:: jenkins2.png 75 | :align: center 76 | 77 | - add a buildstep "Execute Python script" (you need to have the Python 78 | plugin installed and enabled in Jenkins) and enter 79 | ``execfile("jobscript.py")``. 80 | 81 | .. image:: jenkins3.png 82 | :align: center 83 | 84 | - hit "Save" for the new build job. 85 | 86 | You can now ``devpi upload`` a package to an index and see Jenkins starting 87 | after the upload successfully returns. 88 | 89 | Behind the scenes 90 | +++++++++++++++++ 91 | 92 | Once you triggered a job from devpi, you can checkout the ``jobscript.py`` 93 | in the Jenkins workspace to see what was injected. The injected 94 | script roughly follows these steps: 95 | 96 | - retrieves a stable virtualenv release through the devpi root/pypi 97 | index (i.e. use its caching ability) 98 | 99 | - unpack the virtualenv tar ball and run the contained "virtualenv.py" 100 | script to create a ``_devpi`` environment 101 | 102 | - install/upgrade ``devpi-client`` into that environment 103 | 104 | - ``devpi use`` the index which we were triggered from 105 | 106 | - ``devpi test PKG`` where PKG is the package name that we uploaded. 107 | -------------------------------------------------------------------------------- /devpi_jenkins/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '3.0.1' 2 | -------------------------------------------------------------------------------- /devpi_jenkins/devpibootstrap.py.template: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | A jenkins uploadtrigger bootstrap script to automatically install devpi 4 | in a virtualenv and then run "devpi use" and "devpi test". 5 | """ 6 | 7 | __version__ = '1.0' 8 | 9 | import sys 10 | import os 11 | import re 12 | from os import path 13 | import shutil 14 | 15 | from subprocess import Popen, PIPE, check_call, CalledProcessError 16 | import tarfile 17 | 18 | USETOXDEV=os.environ.get('USETOXDEV', False) 19 | 20 | PY3 = sys.version_info[0] == 3 21 | 22 | if PY3: 23 | from urllib.request import urlretrieve 24 | else: 25 | from urllib import urlretrieve 26 | 27 | 28 | def log(msg, *args): 29 | if args: 30 | msg = msg % args 31 | sys.stderr.write("bootstrap: %s\n" % msg) 32 | sys.stderr.flush() 33 | 34 | def run(cmd, shell=True): 35 | """Run the given command in shell""" 36 | log('running command: %s', cmd) 37 | check_call(cmd, shell=shell) 38 | 39 | 40 | def crun(cmd, shell=True): 41 | """Run the given command and return its output""" 42 | log('running command (for output): %s', cmd) 43 | p = Popen(cmd, stdout=PIPE, shell=shell) 44 | stdout, stderr = p.communicate() 45 | return stdout 46 | 47 | 48 | def wget(url): 49 | """Download the given file to current directory""" 50 | log('downloading %s', url) 51 | basename = path.basename(url) 52 | localpath = path.join(os.getcwd(), basename) 53 | urlretrieve(url, localpath) 54 | return localpath 55 | 56 | 57 | def has_script(venv, name): 58 | """Check if the virtualenv has the given script 59 | 60 | Looks for bin/$name (unix) or Scripts/$name.exe (windows) in the virtualenv 61 | """ 62 | if sys.platform == 'win32': 63 | return any([path.exists(path.join(venv, 'Scripts', name)), 64 | path.exists(path.join(venv, 'Scripts', name + '.exe'))]) 65 | else: 66 | return path.exists(path.join(venv, 'bin', name)) 67 | 68 | def activate_path(venv): 69 | """Return the full path to the script virtualenv directory""" 70 | if sys.platform == 'win32': 71 | p = path.abspath(path.join(venv, 'Scripts')) 72 | else: 73 | p = path.abspath(path.join(venv, 'bin')) 74 | assert path.exists(p), p 75 | os.environ['PATH'] = p + os.pathsep + os.environ['PATH'] 76 | log("added to PATH: %s", p) 77 | 78 | def get_script_path(venv, name): 79 | """Return the full path to the script in virtualenv directory""" 80 | if sys.platform == 'win32': 81 | p = path.join(venv, 'Scripts', name) 82 | if not path.exists(p): 83 | p = path.join(venv, 'Scripts', name + '.exe') 84 | else: 85 | p = path.join(venv, 'bin', name) 86 | 87 | if not path.exists(p): 88 | raise NameError('cannot find a script named "%s"' % (name,)) 89 | 90 | return path.abspath(p) 91 | 92 | 93 | def get_package_version(venv, name): 94 | """Return the installed version of a package. """ 95 | py = get_script_path(venv, 'python') 96 | s = 'import devpi,sys; sys.stdout.write(str(%s.__version__))' % name 97 | if sys.version_info[:2] >= (2, 6): 98 | return crun('%s -s -c "%s"' % (py, s)) 99 | else: 100 | return crun('%s -c "%s"' % (py, s)) 101 | 102 | 103 | def ensuredir(p): 104 | if not path.isdir(p): 105 | os.makedirs(p) 106 | 107 | def get_virtualenv(virtualenvtar_url): 108 | basename = path.basename(virtualenvtar_url) 109 | log("matching %s", basename) 110 | dirname = re.match("(virtualenv-.*).tar.gz", basename).group(1) 111 | vdir = path.abspath(dirname) 112 | virtualenv_script = path.join(vdir, "virtualenv.py") 113 | if not path.exists(virtualenv_script): 114 | if path.exists(vdir): 115 | shutil.rmtree(vdir) 116 | archive = wget(virtualenvtar_url) 117 | log("got %s", archive) 118 | tar = tarfile.open(archive, "r:gz") 119 | try: 120 | tar.extractall(filter="data") 121 | except TypeError: 122 | tar.extractall() 123 | tar.close() 124 | assert path.exists(vdir) 125 | log("extracted %s", vdir) 126 | assert path.exists(virtualenv_script) 127 | return virtualenv_script 128 | 129 | class Devpi: 130 | def __init__(self, basedir, script): 131 | self.basedir = basedir 132 | self.script = script 133 | 134 | def __call__(self, *args): 135 | argv = list(args) 136 | assert args 137 | clientdir = path.join(self.basedir, ".devpiclient") 138 | try: 139 | self.run([self.script, "--clientdir=%s" % clientdir] + argv, 140 | shell=False) 141 | except CalledProcessError: 142 | _, e, _ = sys.exc_info() 143 | log('exited with error code %d', e.returncode) 144 | sys.exit(e.returncode) 145 | 146 | def run(self, *args, **kwargs): 147 | return run(*args, **kwargs) 148 | 149 | def activate_devpi_script(TENV, virtualenvtar_url, devpi_install_index): 150 | log('bootstrap version %s', __version__) 151 | log("devpi bootstrap env is/shall be at: %s" % TENV) 152 | 153 | os.environ['PATH'] = TENV + os.path.pathsep + os.environ['PATH'] 154 | # create virtual environment 155 | if not has_script(TENV, 'devpi'): 156 | virtualenv_path = get_virtualenv(virtualenvtar_url) 157 | run([sys.executable, virtualenv_path, TENV], shell=False) 158 | 159 | assert has_script(TENV, 'python'), 'no python script' 160 | assert has_script(TENV, 'pip'), 'no pip script' 161 | activate_path(TENV) 162 | 163 | pip = get_script_path(TENV, 'pip') 164 | 165 | # reinstall always for now 166 | run('%s install --pre -i %s --force-reinstall --upgrade devpi-client' % ( 167 | pip, devpi_install_index)) 168 | assert has_script(TENV, 'devpi') 169 | return get_script_showversion(TENV, "devpi") 170 | 171 | def get_script_showversion(TENV, name): 172 | version = get_package_version(TENV, name) 173 | assert has_script(TENV, name) 174 | script = path.abspath(get_script_path(TENV, name)) 175 | log('%s is installed at %s, version is %s', name, script, version) 176 | return script 177 | 178 | def main(indexurl = "{INDEXURL}", 179 | virtualenvtar_url = "{VIRTUALENVTARURL}", 180 | devpi_install_index = "{DEVPI_INSTALL_INDEX}", 181 | testspec = "{TESTSPEC}"): 182 | log("starting Jenkins job for %s" % testspec) 183 | basedir = os.getcwd() 184 | 185 | # prepare a TMPDIR that resides in the Jenkins workspace 186 | # for installing devpi itself 187 | OLD_TMPDIR = os.environ.get("TMPDIR") 188 | os.environ["TMPDIR"] = tmpdir = path.join(basedir, "TMP") 189 | ensuredir(tmpdir) 190 | 191 | # prepare a virtualenv for installing devpi-client 192 | TENV = path.join(basedir, "_devpi") 193 | script = activate_devpi_script(TENV, virtualenvtar_url, 194 | devpi_install_index) 195 | 196 | devpi = Devpi(basedir, script) 197 | 198 | # invoke the just-installed devpi client 199 | devpi("use", indexurl) 200 | 201 | # reset the TMPDIR because test invokes tox and 202 | # using an in-workspace work dir would create 203 | # very long pathnames which often cause problems 204 | if OLD_TMPDIR: 205 | os.environ["TMPDIR"] = OLD_TMPDIR 206 | else: 207 | del os.environ["TMPDIR"] 208 | devpi("test", testspec) 209 | 210 | if __name__ == '__main__': 211 | main() 212 | -------------------------------------------------------------------------------- /devpi_jenkins/main.py: -------------------------------------------------------------------------------- 1 | from devpi_common.request import new_requests_session 2 | from devpi_jenkins import __version__ 3 | from io import BytesIO 4 | from pluggy import HookimplMarker 5 | import json 6 | 7 | 8 | server_hookimpl = HookimplMarker("devpiserver") 9 | 10 | 11 | def render_string(confname, format=None, **kw): 12 | template = confname + ".template" 13 | from importlib.resources import read_text 14 | templatestring = read_text("devpi_jenkins", template) 15 | 16 | kw = dict((x[0], str(x[1])) for x in kw.items()) 17 | if format is None: 18 | result = templatestring.format(**kw) 19 | else: 20 | result = templatestring % kw 21 | return result 22 | 23 | 24 | @server_hookimpl 25 | def devpiserver_indexconfig_defaults(): 26 | return {"uploadtrigger_jenkins": None} 27 | 28 | 29 | @server_hookimpl 30 | def devpiserver_on_upload_sync(log, application_url, stage, project, version): 31 | jenkinsurl = stage.ixconfig.get("uploadtrigger_jenkins") 32 | if not jenkinsurl: 33 | return 34 | jenkinsurl = jenkinsurl.format(pkgname=project, pkgversion=version) 35 | 36 | source = render_string( 37 | "devpibootstrap.py", 38 | INDEXURL=application_url + "/" + stage.name, 39 | VIRTUALENVTARURL=( 40 | application_url + 41 | "/root/pypi/+f/f61/cdd983d2c4e6a/" 42 | "virtualenv-1.11.6.tar.gz"), 43 | TESTSPEC='%s==%s' % (project, version), 44 | DEVPI_INSTALL_INDEX=application_url + "/" + stage.name + "/+simple/") 45 | inputfile = BytesIO(source.encode("ascii")) 46 | session = new_requests_session(agent=("devpi-jenkins", __version__)) 47 | try: 48 | r = session.post( 49 | jenkinsurl, 50 | data={ 51 | "Submit": "Build", 52 | "name": "jobscript.py", 53 | "json": json.dumps({ 54 | "parameter": {"name": "jobscript.py", "file": "file0"}})}, 55 | files={"file0": ("file0", inputfile)}) 56 | except session.Errors: 57 | raise RuntimeError("%s: failed to connect to jenkins at %s", 58 | project, jenkinsurl) 59 | 60 | if 200 <= r.status_code < 300: 61 | log.info("successfully triggered jenkins: %s", jenkinsurl) 62 | else: 63 | log.error("%s: failed to trigger jenkins at %s", r.status_code, 64 | jenkinsurl) 65 | log.debug(r.content) 66 | raise RuntimeError("%s: failed to trigger jenkins at %s", 67 | project, jenkinsurl) 68 | -------------------------------------------------------------------------------- /jenkins1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devpi/devpi-jenkins/217923823e9449c2c8d31d47ce0ed8a0647271a1/jenkins1.png -------------------------------------------------------------------------------- /jenkins2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devpi/devpi-jenkins/217923823e9449c2c8d31d47ce0ed8a0647271a1/jenkins2.png -------------------------------------------------------------------------------- /jenkins3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devpi/devpi-jenkins/217923823e9449c2c8d31d47ce0ed8a0647271a1/jenkins3.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import os 3 | 4 | 5 | def get_version(path): 6 | fn = os.path.join( 7 | os.path.dirname(os.path.abspath(__file__)), 8 | path, "__init__.py") 9 | with open(fn) as f: 10 | for line in f: 11 | if '__version__' in line: 12 | parts = line.split("=") 13 | return parts[1].split("'")[1] 14 | 15 | 16 | here = os.path.abspath(os.path.dirname(__file__)) 17 | README = open(os.path.join(here, 'README.rst')).read() 18 | CHANGELOG = open(os.path.join(here, 'CHANGELOG.rst')).read() 19 | 20 | 21 | setup( 22 | name="devpi-jenkins", 23 | description="devpi-jenkins: Jenkins build trigger for devpi-server", 24 | long_description=README + "\n\n" + CHANGELOG, 25 | url="https://github.com/devpi/devpi-jenkins", 26 | version=get_version("devpi_jenkins"), 27 | maintainer="Florian Schulze", 28 | maintainer_email="mail@florian-schulze.net", 29 | license="MIT", 30 | classifiers=[ 31 | "Environment :: Web Environment", 32 | "Intended Audience :: Developers", 33 | "Intended Audience :: System Administrators", 34 | "License :: OSI Approved :: MIT License", 35 | "Programming Language :: Python"] + [ 36 | "Programming Language :: Python :: %s" % x 37 | for x in "3 3.8 3.9 3.10 3.11 3.12".split()], 38 | entry_points={ 39 | 'devpi_server': [ 40 | "devpi-jenkins = devpi_jenkins.main"]}, 41 | install_requires=[ 42 | 'devpi-server>=5.2.0'], 43 | include_package_data=True, 44 | python_requires='>=3.8', 45 | zip_safe=False, 46 | packages=['devpi_jenkins']) 47 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from devpi_common.metadata import parse_version 2 | from devpi_server import __version__ as _devpi_server_version 3 | 4 | 5 | devpi_server_version = parse_version(_devpi_server_version) 6 | 7 | 8 | if devpi_server_version < parse_version("6.9.3dev"): 9 | from test_devpi_server.conftest import gentmp, httpget, makemapp # noqa 10 | from test_devpi_server.conftest import maketestapp, makexom, mapp # noqa 11 | from test_devpi_server.conftest import pypiurls, testapp # noqa 12 | from test_devpi_server.conftest import storage_info # noqa 13 | from test_devpi_server.conftest import mock, reqmock # noqa 14 | else: 15 | pytest_plugins = ["pytest_devpi_server", "test_devpi_server.plugin"] 16 | -------------------------------------------------------------------------------- /tests/test_devpibootstrap.py: -------------------------------------------------------------------------------- 1 | from devpi_jenkins.main import render_string 2 | import os 3 | import pytest 4 | import shutil 5 | import tarfile 6 | 7 | 8 | bootstrapindex = "http://localhost:3141/root/dev/+simple/" 9 | 10 | 11 | @pytest.fixture(scope="session") 12 | def bootstrapdict(): 13 | source = render_string( 14 | "devpibootstrap.py", 15 | INDEXURL="http://localhost:3141/root/dev", 16 | VIRTUALENVTARURL="http://localhost:3141/root/pypi/virtualenv-1.10.tar.gz", 17 | DEVPI_INSTALL_INDEX="http://localhost:3141/root/dev", 18 | TESTSPEC="pytest") 19 | d = {} 20 | exec(compile(source, "", "exec"), d) 21 | return d 22 | 23 | 24 | @pytest.fixture(scope="session") 25 | def url_of_liveserver(request): 26 | import random 27 | import subprocess 28 | port = random.randint(2001, 64000) 29 | clientdir = request.config._tmpdirhandler.mktemp("liveserver") 30 | subprocess.check_call(["devpi-server", "--serverdir", str(clientdir), 31 | "--port", str(port), "--start"]) 32 | 33 | def stop(): 34 | subprocess.check_call(["devpi-server", "--serverdir", str(clientdir), 35 | "--stop"]) 36 | 37 | request.addfinalizer(stop) 38 | return "http://localhost:%s" % port 39 | 40 | 41 | @pytest.fixture 42 | def virtualenv_tar(tmpdir): 43 | base = tmpdir.mkdir("virtualenv_build") 44 | script = base.ensure("virtualenv-1.10", "virtualenv.py") 45 | script.write("#") 46 | tarpath = base.join("virtualenv-1.10.tar.gz") 47 | tar = tarfile.open(tarpath.strpath, "w:gz") 48 | tar.add(str(script), script.relto(base)) 49 | tar.close() 50 | print("created %s" % tarpath.strpath) 51 | return tarpath.strpath 52 | 53 | 54 | def test_bootstrapdict_create(bootstrapdict): 55 | assert "Devpi" in bootstrapdict 56 | 57 | 58 | def test_get_virtualenv(tmpdir, bootstrapdict, virtualenv_tar, monkeypatch): 59 | monkeypatch.chdir(tmpdir) 60 | get_virtualenv = bootstrapdict["get_virtualenv"] 61 | vurl = "http://localhost:3141/root/pypi/virtualenv-1.10.tar.gz" 62 | 63 | def urlretrieve(url, localpath): 64 | assert url == vurl 65 | shutil.copy(virtualenv_tar, localpath) 66 | 67 | monkeypatch.setitem(bootstrapdict, "urlretrieve", urlretrieve) 68 | virtualenv_script = get_virtualenv(vurl) 69 | assert os.path.exists(virtualenv_script) 70 | # check that a second attempt won't hit the web 71 | monkeypatch.setitem(bootstrapdict, "wget", None) 72 | virtualenv_script = get_virtualenv(vurl) 73 | assert os.path.exists(virtualenv_script) 74 | 75 | 76 | @pytest.mark.xfail(reason="cannot provide current devpi-server " 77 | "safely in an index") 78 | def test_main(request, url_of_liveserver, mapp, tmpdir, monkeypatch): 79 | # not a very good test as it requires going to pypi.python.org 80 | mapp.login_root() 81 | mapp.create_index("root/dev") 82 | tmpdir.chdir() 83 | source = render_string( 84 | "devpibootstrap.py", 85 | INDEXURL=url_of_liveserver + "/root/dev", 86 | VIRTUALENVTARURL=("https://pypi.python.org/packages/source/" 87 | "v/virtualenv/virtualenv-1.10.tar.gz"), 88 | DEVPI_INSTALL_INDEX=url_of_liveserver + "/root/dev/+simple/", 89 | TESTSPEC="py") 90 | d = {} 91 | exec(compile(source, "", "exec"), d) 92 | l = [] 93 | 94 | def record(*args, **kwargs): 95 | l.append(args) 96 | 97 | monkeypatch.setattr(d["Devpi"], "run", record) 98 | d["main"]() 99 | assert len(l) == 2 100 | assert "use" in l[0][1] 101 | assert "test" in l[1][1] 102 | -------------------------------------------------------------------------------- /tests/test_jenkins.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def xom(makexom): 6 | from devpi_jenkins import main 7 | xom = makexom(plugins=[(main, None)]) 8 | return xom 9 | 10 | 11 | @pytest.mark.notransaction 12 | def test_create_index_with_jenkinsurl(mapp): 13 | url = "http://localhost:8080/" 14 | mapp.login_root() 15 | mapp.create_index("root/test3") 16 | mapp.use("root/test3") 17 | mapp.set_indexconfig_option("uploadtrigger_jenkins", url) 18 | data = mapp.getjson("/root/test3") 19 | assert data["result"]["uploadtrigger_jenkins"] == url 20 | 21 | 22 | @pytest.mark.notransaction 23 | def test_upload_with_jenkins(mapp, reqmock): 24 | from io import BytesIO 25 | import cgi 26 | import json 27 | mapp.create_and_use() 28 | mapp.set_indexconfig_option("uploadtrigger_jenkins", "http://x.com/{pkgname}/{pkgversion}") 29 | rec = reqmock.mockresponse(code=200, url=None) 30 | mapp.upload_file_pypi("pkg1-2.6.tgz", b"123", "pkg1", "2.6", code=200) 31 | assert len(rec.requests) == 1 32 | req = rec.requests[0] 33 | assert req.url == "http://x.com/pkg1/2.6" 34 | fs = cgi.FieldStorage( 35 | BytesIO(req.body), req.headers, environ=dict(REQUEST_METHOD='POST')) 36 | assert fs.getfirst("Submit") == "Build" 37 | assert json.loads(fs.getfirst("json")) == { 38 | "parameter": {"file": "file0", "name": "jobscript.py"}} 39 | assert fs.getfirst("name") == "jobscript.py" 40 | script = fs.getfirst("file0") 41 | assert script.startswith(b'#!/') 42 | assert b'indexurl = "http://localhost/user1/dev"' in script 43 | assert b'virtualenvtar_url = "http://localhost/root/pypi/+f/f61/cdd983d2c4e6a/virtualenv-1.11.6.tar.gz"' in script 44 | assert b'devpi_install_index = "http://localhost/user1/dev/+simple/"' in script 45 | assert b'testspec = "pkg1==2.6"' in script 46 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{38,312}{,-server520} 3 | 4 | 5 | [testenv] 6 | commands = py.test --cov {envsitepackagesdir}/devpi_jenkins 7 | deps = 8 | webtest 9 | mock 10 | pytest 11 | pytest-cov 12 | server520: devpi-server==5.2.0 13 | server520: ruamel.yaml 14 | server520: pyramid<2 15 | 16 | 17 | [pytest] 18 | addopts = 19 | --cov-report=term 20 | --cov-report=html 21 | -r a 22 | -W error::DeprecationWarning 23 | -W ignore:"`pyramid.compat` is deprecated":DeprecationWarning 24 | -W ignore:"Accessing argon2.__version__":DeprecationWarning 25 | -W ignore:"Deprecated call to `pkg_resources.declare_namespace":DeprecationWarning 26 | -W ignore:"pkg_resources is deprecated":DeprecationWarning 27 | -W once:"'cgi' is deprecated":DeprecationWarning 28 | -W once:"'crypt' is deprecated":DeprecationWarning 29 | -W once:"open_text is deprecated":DeprecationWarning 30 | -W once:"read_text is deprecated":DeprecationWarning 31 | -W once:"setDaemon() is deprecated":DeprecationWarning 32 | -W once::pytest.PytestDeprecationWarning 33 | -W once::ResourceWarning 34 | norecursedirs = bin lib include Scripts 35 | --------------------------------------------------------------------------------