diff --git a/datasette/publish/heroku.py b/datasette/publish/heroku.py index 171252ce..f576a346 100644 --- a/datasette/publish/heroku.py +++ b/datasette/publish/heroku.py @@ -3,7 +3,9 @@ from datasette import hookimpl import click import json import os +import pathlib import shlex +import shutil from subprocess import call, check_output import tempfile @@ -28,6 +30,11 @@ def publish_subcommand(publish): "--tar", help="--tar option to pass to Heroku, e.g. --tar=/usr/local/bin/gtar", ) + @click.option( + "--generate-dir", + type=click.Path(dir_okay=True, file_okay=False), + help="Output generated application files and stop without deploying", + ) def heroku( files, metadata, @@ -49,6 +56,7 @@ def publish_subcommand(publish): about_url, name, tar, + generate_dir, ): "Publish databases to Datasette running on Heroku" fail_if_publish_binary_not_installed( @@ -105,6 +113,16 @@ def publish_subcommand(publish): secret, extra_metadata, ): + if generate_dir: + # Recursively copy files from current working directory to it + if pathlib.Path(generate_dir).exists(): + raise click.ClickException("Directory already exists") + shutil.copytree(".", generate_dir) + click.echo( + f"Generated files written to {generate_dir}, stopping without deploying", + err=True, + ) + return app_name = None if name: # Check to see if this app already exists @@ -176,7 +194,7 @@ def temporary_heroku_directory( fp.write(json.dumps(metadata_content, indent=2)) with open("runtime.txt", "w") as fp: - fp.write("python-3.8.10") + fp.write("python-3.11.0") if branch: install = [ diff --git a/datasette/version.py b/datasette/version.py index 3a4f06dc..6016687a 100644 --- a/datasette/version.py +++ b/datasette/version.py @@ -1,2 +1,2 @@ -__version__ = "0.63.1" +__version__ = "0.63.2" __version_info__ = tuple(__version__.split(".")) diff --git a/docs/changelog.rst b/docs/changelog.rst index 0e0393ef..865bb58e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,14 @@ Changelog ========= +.. _v0_63_2: + +0.63.2 (2022-11-18) +------------------- + +- Fixed a bug in ``datasette publish heroku`` where deployments failed due to an older version of Python being requested. (:issue:`1905`) +- New ``datasette publish heroku --generate-dir `` option for generating a Heroku deployment directory without deploying it. + .. _v0_63_1: 0.63.1 (2022-11-10) diff --git a/docs/cli-reference.rst b/docs/cli-reference.rst index 4a8465cb..a6885fc8 100644 --- a/docs/cli-reference.rst +++ b/docs/cli-reference.rst @@ -501,6 +501,8 @@ See :ref:`publish_heroku`. -n, --name TEXT Application name to use when deploying --tar TEXT --tar option to pass to Heroku, e.g. --tar=/usr/local/bin/gtar + --generate-dir DIRECTORY Output generated application files and stop + without deploying --help Show this message and exit. diff --git a/docs/publish.rst b/docs/publish.rst index 4ba94792..7ae0399e 100644 --- a/docs/publish.rst +++ b/docs/publish.rst @@ -73,6 +73,10 @@ This will output some details about the new deployment, including a URL like thi You can specify a custom app name by passing ``-n my-app-name`` to the publish command. This will also allow you to overwrite an existing app. +Rather than deploying directly you can use the ``--generate-dir`` option to output the files that would be deployed to a directory:: + + datasette publish heroku mydatabase.db --generate-dir=/tmp/deploy-this-to-heroku + See :ref:`cli_help_publish_heroku___help` for the full list of options for this command. .. _publish_vercel: diff --git a/test-in-pyodide-with-shot-scraper.sh b/test-in-pyodide-with-shot-scraper.sh index e5df7398..0c140818 100755 --- a/test-in-pyodide-with-shot-scraper.sh +++ b/test-in-pyodide-with-shot-scraper.sh @@ -25,6 +25,7 @@ async () => { let output = await pyodide.runPythonAsync(\` import micropip await micropip.install('h11==0.12.0') + await micropip.install('httpx==0.23') await micropip.install('http://localhost:8529/$wheel') import ssl import setuptools diff --git a/tests/test_publish_heroku.py b/tests/test_publish_heroku.py index b5a8af73..cab83654 100644 --- a/tests/test_publish_heroku.py +++ b/tests/test_publish_heroku.py @@ -2,6 +2,7 @@ from click.testing import CliRunner from datasette import cli from unittest import mock import os +import pathlib import pytest @@ -128,3 +129,55 @@ def test_publish_heroku_plugin_secrets( mock.call(["heroku", "builds:create", "-a", "f", "--include-vcs-ignore"]), ] ) + + +@pytest.mark.serial +@mock.patch("shutil.which") +@mock.patch("datasette.publish.heroku.check_output") +@mock.patch("datasette.publish.heroku.call") +def test_publish_heroku_generate_dir( + mock_call, mock_check_output, mock_which, tmp_path_factory +): + mock_which.return_value = True + mock_check_output.side_effect = lambda s: { + "['heroku', 'plugins']": b"heroku-builds", + }[repr(s)] + runner = CliRunner() + os.chdir(tmp_path_factory.mktemp("runner")) + with open("test.db", "w") as fp: + fp.write("data") + output = str(tmp_path_factory.mktemp("generate_dir") / "output") + result = runner.invoke( + cli.cli, + [ + "publish", + "heroku", + "test.db", + "--generate-dir", + output, + ], + ) + assert result.exit_code == 0 + path = pathlib.Path(output) + assert path.exists() + file_names = {str(r.relative_to(path)) for r in path.glob("*")} + assert file_names == { + "requirements.txt", + "bin", + "runtime.txt", + "Procfile", + "test.db", + } + for name, expected in ( + ("requirements.txt", "datasette"), + ("runtime.txt", "python-3.11.0"), + ( + "Procfile", + ( + "web: datasette serve --host 0.0.0.0 -i test.db " + "--cors --port $PORT --inspect-file inspect-data.json" + ), + ), + ): + with open(path / name) as fp: + assert fp.read().strip() == expected