first commit
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""Static deployment-readiness checks (these do NOT deploy anything to Heroku)."""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
# Top-level keys documented for app.json (https://devcenter.heroku.com/articles/app-json-schema)
|
||||
APP_JSON_KEYS = {
|
||||
"name", "description", "keywords", "website", "repository", "logo", "success_url", "scripts", "env",
|
||||
"formation", "image", "addons", "buildpacks", "environments", "stack",
|
||||
}
|
||||
ENV_KEYS = {"description", "value", "required", "generator"}
|
||||
|
||||
|
||||
def _config_vars() -> set[str]:
|
||||
source = (ROOT / "config.py").read_text()
|
||||
return set(re.findall(r'r\.\w+\(\s*"([A-Z0-9_]+)"', source)) | set(re.findall(r'env\.get\("([A-Z0-9_]+)"\)', source))
|
||||
|
||||
|
||||
def _app_json() -> dict:
|
||||
return json.loads((ROOT / "app.json").read_text())
|
||||
|
||||
|
||||
def test_app_json_structure():
|
||||
manifest = _app_json()
|
||||
assert set(manifest) <= APP_JSON_KEYS
|
||||
assert len(manifest["name"]) <= 30 and manifest["description"]
|
||||
assert manifest["buildpacks"] == [{"url": "heroku/python"}]
|
||||
assert manifest["formation"] == {"worker": {"quantity": 1, "size": "basic"}}
|
||||
assert manifest["addons"][0]["plan"].startswith("heroku-postgresql:")
|
||||
for name, spec in manifest["env"].items():
|
||||
assert set(spec) <= ENV_KEYS, name
|
||||
assert spec["description"], name
|
||||
assert isinstance(spec.get("required", False), bool), name
|
||||
if "value" in spec:
|
||||
assert isinstance(spec["value"], str), name
|
||||
|
||||
|
||||
def test_app_json_only_lists_supported_variables():
|
||||
supported = _config_vars()
|
||||
unsupported = set(_app_json()["env"]) - supported
|
||||
assert not unsupported, f"app.json lists variables the app does not read: {unsupported}"
|
||||
|
||||
|
||||
def test_env_example_documents_every_variable():
|
||||
documented = set(re.findall(r"^([A-Z0-9_]+)=", (ROOT / ".env.example").read_text(), re.MULTILINE))
|
||||
supported = _config_vars() - {"DYNO"} # DYNO is set by Heroku itself
|
||||
assert supported - documented == set()
|
||||
assert documented - supported == set()
|
||||
|
||||
|
||||
def test_procfile_matches_entry_point():
|
||||
procfile = (ROOT / "Procfile").read_text().strip().splitlines()
|
||||
assert procfile == ["worker: python main.py run"]
|
||||
assert set(_app_json()["formation"]) == {line.split(":")[0] for line in procfile}
|
||||
main_source = (ROOT / "main.py").read_text()
|
||||
assert 'sub.add_parser("run"' in main_source and "signal.SIGTERM" in main_source
|
||||
|
||||
|
||||
def test_runtime_and_requirements():
|
||||
assert (ROOT / ".python-version").read_text().strip() == "3.12"
|
||||
assert not (ROOT / "runtime.txt").exists() # deprecated in favour of .python-version
|
||||
reqs = (ROOT / "requirements.txt").read_text()
|
||||
for package in ("Telethon", "google-api-python-client", "google-auth", "google-auth-oauthlib", "psycopg[binary]", "python-dotenv"):
|
||||
assert re.search(rf"^{re.escape(package)}==", reqs, re.MULTILINE), package
|
||||
|
||||
|
||||
def test_gitignore_protects_secrets_and_state():
|
||||
ignored = (ROOT / ".gitignore").read_text().splitlines()
|
||||
for pattern in (".env", "*.session", "credentials/", "token.json", "session_string.txt", "data/", "downloads/", "*.db"):
|
||||
assert pattern in ignored, pattern
|
||||
|
||||
|
||||
def test_readme_deploy_button():
|
||||
readme = (ROOT / "README.md").read_text()
|
||||
assert "https://www.herokucdn.com/deploy/button.svg" in readme
|
||||
assert re.search(r"\]\(https://heroku\.com/deploy\?template=https://github\.com/[^)\s]+\)", readme)
|
||||
Reference in New Issue
Block a user