diff --git a/docs/advanced.rst b/docs/advanced.rst index 19f38377..e48d94c2 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -133,6 +133,12 @@ Or you can install packages exactly as specified in ``Pipfile.lock`` using the ` ``pipenv install --ignore-pipfile`` is nearly equivalent to ``pipenv sync``, but ``pipenv sync`` will *never* attempt to re-lock your dependencies as it is considered an atomic operation. ``pipenv install`` by default does attempt to re-lock unless using the ``--deploy`` flag. +You may only wish to verify your ``Pipfile.lock`` is up-to-date with dependencies specified in the ``Pipfile``, without installing:: + + $ pipenv verify + +The command will perform a verification, and return an exit code ``1`` when dependency locking is needed. This may be useful for cases when the ``Pipfile.lock`` file is subject to version control, so this command can be used within your CI/CD pipelines. + Deploying System Dependencies ///////////////////////////// diff --git a/news/4893.feature.rst b/news/4893.feature.rst new file mode 100644 index 00000000..ea2ab4dd --- /dev/null +++ b/news/4893.feature.rst @@ -0,0 +1 @@ +New CLI command ``verify``, checks the Pipfile.lock is up-to-date \ No newline at end of file diff --git a/pipenv/cli/command.py b/pipenv/cli/command.py index a3910b22..ae99b6ba 100644 --- a/pipenv/cli/command.py +++ b/pipenv/cli/command.py @@ -700,5 +700,27 @@ def scripts(state): echo("\n".join(fix_utf8(line) for line in lines)) +@cli.command( + short_help="Verify the hash in Pipfile.lock is up-to-date.", + context_settings=CONTEXT_SETTINGS, +) +@pass_state +def verify(state): + """Verify the hash in Pipfile.lock is up-to-date.""" + if not state.project.pipfile_exists: + echo("No Pipfile present at project home.", err=True) + sys.exit(1) + if state.project.get_lockfile_hash() != state.project.calculate_pipfile_hash(): + echo( + 'Pipfile.lock is out-of-date. Run {} to update.'.format( + crayons.yellow("$ pipenv lock", bold=True) + ), + err=True + ) + sys.exit(1) + echo(crayons.green('Pipfile.lock is up-to-date.')) + sys.exit(0) + + if __name__ == "__main__": cli() diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index ca201f2a..b2184b1f 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -3,7 +3,7 @@ import os import re - +from pathlib import Path import pytest from flaky import flaky @@ -291,3 +291,44 @@ sqlalchemy = "<=1.2.3" f.write(contents) c = p.pipenv('update --pre --outdated') assert c.returncode == 0 + + +@pytest.mark.cli +def test_pipenv_verify_without_pipfile(PipenvInstance): + with PipenvInstance(pipfile=False) as p: + c = p.pipenv('verify') + assert c.returncode == 1 + assert 'No Pipfile present at project home.' in c.stderr + + +@pytest.mark.cli +def test_pipenv_verify_without_pipfile_lock(PipenvInstance): + with PipenvInstance() as p: + c = p.pipenv('verify') + assert c.returncode == 1 + assert 'Pipfile.lock is out-of-date.' in c.stderr + + +@pytest.mark.cli +def test_pipenv_verify_locked_passing(PipenvInstance): + with PipenvInstance() as p: + p.pipenv('lock') + c = p.pipenv('verify') + assert c.returncode == 0 + assert 'Pipfile.lock is up-to-date.' in c.stdout + + +@pytest.mark.cli +def test_pipenv_verify_locked_outdated_failing(PipenvInstance): + with PipenvInstance() as p: + p.pipenv('lock') + + # modify the Pipfile + pf = Path(p.path).joinpath('Pipfile') + pf_data = pf.read_text() + pf_new = re.sub(r'\[packages\]', '[packages]\nrequests = "*"', pf_data) + pf.write_text(pf_new) + + c = p.pipenv('verify') + assert c.returncode == 1 + assert 'Pipfile.lock is out-of-date.' in c.stderr