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