djaploy 1.2.6__tar.gz → 1.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {djaploy-1.2.6 → djaploy-1.2.7}/PKG-INFO +1 -1
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/systemd/infra/djaploy_hooks.py +11 -6
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/infra/djaploy_hooks.py +47 -4
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy.egg-info/PKG-INFO +1 -1
- {djaploy-1.2.6 → djaploy-1.2.7}/pyproject.toml +1 -1
- {djaploy-1.2.6 → djaploy-1.2.7}/tests/test_bluegreen.py +117 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/LICENSE +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/README.md +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/app.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/borg/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/borg/infra/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/borg/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/janitor/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/janitor/apps.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/janitor/infra/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/janitor/infra/commands/createjanitoruser.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/janitor/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/nginx/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/nginx/apps.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/nginx/infra/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/nginx/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/rclone/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/rclone/apps.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/rclone/infra/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/rclone/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/sync_certs/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/sync_certs/apps.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/sync_certs/infra/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/sync_certs/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/systemd/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/systemd/apps.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/systemd/infra/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/tailscale/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/tailscale/apps.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/tailscale/infra/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/tailscale/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/versioning/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/versioning/apps.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/versioning/infra/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/apps/versioning/infra/djaploy_hooks.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/artifact.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/bin/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/bin/django_pyinfra.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/bin/gunicornherder.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/builtin_hooks.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/certificates.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/changelog.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/commands/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/commands/_utils.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/commands/activate.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/commands/configure.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/commands/deploy.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/commands/restore.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/commands/rollback.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/commands/status.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/commands/sync_certs.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/config.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/deploy.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/discovery.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/hooks.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/infra/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/infra/bluegreen.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/infra/templates.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/infra/utils.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/management/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/management/commands/__init__.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/management/commands/djaploy.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/management/commands/restore_backup.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/management/commands/sync_certs.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/management/commands/update_certs.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/management/commands/verify.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/management/utils.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/notifications.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/utils.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/version.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy/versioning.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy.egg-info/SOURCES.txt +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy.egg-info/dependency_links.txt +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy.egg-info/entry_points.txt +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy.egg-info/requires.txt +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/djaploy.egg-info/top_level.txt +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/setup.cfg +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/tests/test_borg.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/tests/test_config.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/tests/test_deploy_scripts.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/tests/test_discovery.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/tests/test_gunicornherder.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/tests/test_hooks.py +0 -0
- {djaploy-1.2.6 → djaploy-1.2.7}/tests/test_versioning.py +0 -0
|
@@ -34,15 +34,20 @@ def start_services(host_data, artifact_path):
|
|
|
34
34
|
if not target_slot:
|
|
35
35
|
return
|
|
36
36
|
|
|
37
|
+
from pyinfra.operations import server
|
|
37
38
|
for service in getattr(host_data, "services", []):
|
|
38
39
|
slot_service = get_slot_service_name(service, target_slot)
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
# Only start if the service unit file exists — custom services
|
|
42
|
+
# (e.g. streaming) may be rendered by project hooks, not djaploy core.
|
|
43
|
+
server.shell(
|
|
44
|
+
name=f"Start {slot_service} if unit exists",
|
|
45
|
+
commands=[
|
|
46
|
+
f"test -f /etc/systemd/system/{slot_service}.service && "
|
|
47
|
+
f"systemctl enable {slot_service} && "
|
|
48
|
+
f"systemctl restart {slot_service} || "
|
|
49
|
+
f"echo 'Unit {slot_service}.service not found, skipping'",
|
|
50
|
+
],
|
|
46
51
|
_sudo=True,
|
|
47
52
|
)
|
|
48
53
|
|
|
@@ -648,8 +648,13 @@ def activate_release(host_data, artifact_path):
|
|
|
648
648
|
run_migrations(app_user, build_path, host_data)
|
|
649
649
|
collect_static(app_user, build_path, host_data)
|
|
650
650
|
|
|
651
|
-
# Update state.json on the remote server
|
|
651
|
+
# Update state.json on the remote server.
|
|
652
|
+
# Serialize release_name and commit with json.dumps to safely
|
|
653
|
+
# handle quotes and special characters in branch names / messages.
|
|
654
|
+
import json as _json
|
|
652
655
|
commit = getattr(host_data, 'commit', 'unknown')
|
|
656
|
+
safe_release = _json.dumps(release_name)
|
|
657
|
+
safe_commit = _json.dumps(commit)
|
|
653
658
|
socket_path = get_slot_socket_path(app_name, target_slot)
|
|
654
659
|
server.shell(
|
|
655
660
|
name="Update state.json with deployment info",
|
|
@@ -660,8 +665,8 @@ def activate_release(host_data, artifact_path):
|
|
|
660
665
|
f"python_path = venv_path + '/bin/python' if venv_path != 'unknown' else 'unknown'\n"
|
|
661
666
|
f"s = json.load(open('{state_file}'))\n"
|
|
662
667
|
f"s['slots']['{target_slot}'] = {{\n"
|
|
663
|
-
f" 'release':
|
|
664
|
-
f" 'commit':
|
|
668
|
+
f" 'release': {safe_release},\n"
|
|
669
|
+
f" 'commit': {safe_commit},\n"
|
|
665
670
|
f" 'deployed_at': datetime.datetime.now(datetime.timezone.utc).isoformat(),\n"
|
|
666
671
|
f" 'python_interpreter': python_path,\n"
|
|
667
672
|
f" 'venv_path': venv_path,\n"
|
|
@@ -835,6 +840,7 @@ def activate_bluegreen(host_data):
|
|
|
835
840
|
from pyinfra.facts.server import Command
|
|
836
841
|
from djaploy.infra.utils import (
|
|
837
842
|
is_bluegreen, get_app_path, get_slot_socket_path,
|
|
843
|
+
get_slot_service_name,
|
|
838
844
|
)
|
|
839
845
|
from djaploy.infra.bluegreen import (
|
|
840
846
|
read_active_slot_cmd, set_active_slot_cmd, other_slot,
|
|
@@ -887,6 +893,44 @@ def activate_bluegreen(host_data):
|
|
|
887
893
|
# Store on host.data so activate:post hooks can read it
|
|
888
894
|
host.data._bluegreen_activated_slot = target_slot
|
|
889
895
|
|
|
896
|
+
# Health check: verify all target slot services are running before
|
|
897
|
+
# switching nginx. Abort activation if any service is down.
|
|
898
|
+
services = getattr(host_data, "services", []) or []
|
|
899
|
+
failed_checks = []
|
|
900
|
+
for svc in services:
|
|
901
|
+
slot_svc = get_slot_service_name(svc, target_slot)
|
|
902
|
+
server.shell(
|
|
903
|
+
name=f"Health check: verify {slot_svc} is active",
|
|
904
|
+
commands=[
|
|
905
|
+
f"if systemctl is-active {slot_svc}.service > /dev/null 2>&1; then "
|
|
906
|
+
f"echo 'Health check passed: {slot_svc} is active'; "
|
|
907
|
+
f"else "
|
|
908
|
+
f"echo 'ACTIVATION ABORTED: {slot_svc} is not running.' && "
|
|
909
|
+
f"echo 'Check: systemctl status {slot_svc}.service' && "
|
|
910
|
+
f"exit 1; "
|
|
911
|
+
f"fi",
|
|
912
|
+
],
|
|
913
|
+
_sudo=True,
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
# Extra gunicorn-specific check: verify the socket is responding
|
|
917
|
+
socket_path = get_slot_socket_path(app_name, target_slot)
|
|
918
|
+
server.shell(
|
|
919
|
+
name=f"Health check: verify {target_slot} gunicorn socket responds",
|
|
920
|
+
commands=[
|
|
921
|
+
f"if curl -sf --max-time 5 --unix-socket {socket_path} http://localhost/ > /dev/null 2>&1; then "
|
|
922
|
+
f"echo 'Health check passed: gunicorn socket is responding'; "
|
|
923
|
+
f"elif [ -S {socket_path} ]; then "
|
|
924
|
+
f"echo 'Health check: socket exists, HTTP may be non-200 (proceeding)'; "
|
|
925
|
+
f"else "
|
|
926
|
+
f"echo 'ACTIVATION ABORTED: gunicorn socket not responding.' && "
|
|
927
|
+
f"echo 'Socket: {socket_path}' && "
|
|
928
|
+
f"exit 1; "
|
|
929
|
+
f"fi",
|
|
930
|
+
],
|
|
931
|
+
_sudo=True,
|
|
932
|
+
)
|
|
933
|
+
|
|
890
934
|
# Update nginx upstream config to point to target slot
|
|
891
935
|
_update_nginx_upstream(host_data, app_name, target_slot)
|
|
892
936
|
|
|
@@ -899,7 +943,6 @@ def activate_bluegreen(host_data):
|
|
|
899
943
|
|
|
900
944
|
# Update state.json on remote
|
|
901
945
|
prev_slot = active_slot or "none"
|
|
902
|
-
socket_path = get_slot_socket_path(app_name, target_slot)
|
|
903
946
|
server.shell(
|
|
904
947
|
name=f"Set active slot to {target_slot}",
|
|
905
948
|
commands=[set_active_slot_cmd(state_file, target_slot)],
|
|
@@ -483,5 +483,122 @@ class TestBluegreenHookDiscovery(unittest.TestCase):
|
|
|
483
483
|
global_registry._hooks.update(saved_hooks)
|
|
484
484
|
|
|
485
485
|
|
|
486
|
+
class TestBluegreenHealthCheck(unittest.TestCase):
|
|
487
|
+
"""Test health check logic in activate hook."""
|
|
488
|
+
|
|
489
|
+
def test_health_check_socket_path_uses_target_slot(self):
|
|
490
|
+
"""Health check should curl the target slot's socket."""
|
|
491
|
+
from djaploy.infra.utils import get_slot_socket_path
|
|
492
|
+
blue_socket = get_slot_socket_path("myapp", "blue")
|
|
493
|
+
green_socket = get_slot_socket_path("myapp", "green")
|
|
494
|
+
self.assertEqual(blue_socket, "/run/myapp-blue/myapp.sock")
|
|
495
|
+
self.assertEqual(green_socket, "/run/myapp-green/myapp.sock")
|
|
496
|
+
# Sockets must be different so health check targets the right slot
|
|
497
|
+
self.assertNotEqual(blue_socket, green_socket)
|
|
498
|
+
|
|
499
|
+
def test_health_check_service_name_uses_target_slot(self):
|
|
500
|
+
"""Health check fallback should check the target slot's service."""
|
|
501
|
+
from djaploy.infra.utils import get_slot_service_name
|
|
502
|
+
self.assertEqual(get_slot_service_name("myapp", "blue"), "myapp-blue")
|
|
503
|
+
self.assertEqual(get_slot_service_name("myapp", "green"), "myapp-green")
|
|
504
|
+
|
|
505
|
+
def test_health_check_command_structure(self):
|
|
506
|
+
"""Verify the health check shell command contains the expected elements."""
|
|
507
|
+
# Simulate what activate_bluegreen generates
|
|
508
|
+
app_name = "testapp"
|
|
509
|
+
target_slot = "green"
|
|
510
|
+
from djaploy.infra.utils import get_slot_socket_path
|
|
511
|
+
socket_path = get_slot_socket_path(app_name, target_slot)
|
|
512
|
+
|
|
513
|
+
# Build the command the same way the hook does
|
|
514
|
+
cmd = (
|
|
515
|
+
f"if curl -sf --max-time 5 --unix-socket {socket_path} http://localhost/ > /dev/null 2>&1; then "
|
|
516
|
+
f"echo 'Health check passed: {target_slot} slot is responding'; "
|
|
517
|
+
f"else "
|
|
518
|
+
f"if [ -S {socket_path} ] && systemctl is-active {app_name}-{target_slot}.service > /dev/null 2>&1; then "
|
|
519
|
+
f"echo 'Health check: {target_slot} slot service is active (HTTP response may be non-200, proceeding)'; "
|
|
520
|
+
f"else "
|
|
521
|
+
f"echo 'ACTIVATION ABORTED: {target_slot} slot is not responding.' && "
|
|
522
|
+
f"echo 'Socket: {socket_path}' && "
|
|
523
|
+
f"echo 'Check: systemctl status {app_name}-{target_slot}.service' && "
|
|
524
|
+
f"exit 1; "
|
|
525
|
+
f"fi; "
|
|
526
|
+
f"fi"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Must curl the correct socket
|
|
530
|
+
self.assertIn(f"--unix-socket /run/testapp-green/testapp.sock", cmd)
|
|
531
|
+
# Must check the correct service on fallback
|
|
532
|
+
self.assertIn("systemctl is-active testapp-green.service", cmd)
|
|
533
|
+
# Must abort with exit 1 on failure
|
|
534
|
+
self.assertIn("exit 1", cmd)
|
|
535
|
+
# Must include the abort message
|
|
536
|
+
self.assertIn("ACTIVATION ABORTED", cmd)
|
|
537
|
+
|
|
538
|
+
def test_health_check_different_slots_produce_different_commands(self):
|
|
539
|
+
"""Blue and green health checks should target different sockets."""
|
|
540
|
+
from djaploy.infra.utils import get_slot_socket_path
|
|
541
|
+
blue_cmd = f"curl --unix-socket {get_slot_socket_path('app', 'blue')}"
|
|
542
|
+
green_cmd = f"curl --unix-socket {get_slot_socket_path('app', 'green')}"
|
|
543
|
+
self.assertIn("app-blue", blue_cmd)
|
|
544
|
+
self.assertIn("app-green", green_cmd)
|
|
545
|
+
self.assertNotEqual(blue_cmd, green_cmd)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
class TestBluegreenServiceExistenceCheck(unittest.TestCase):
|
|
549
|
+
"""Test that start_services checks unit file existence before starting."""
|
|
550
|
+
|
|
551
|
+
def test_service_check_command_structure(self):
|
|
552
|
+
"""The start command should test for the unit file before starting."""
|
|
553
|
+
from djaploy.infra.utils import get_slot_service_name
|
|
554
|
+
slot_service = get_slot_service_name("myapp", "blue")
|
|
555
|
+
|
|
556
|
+
# Build the command the same way start_services does
|
|
557
|
+
cmd = (
|
|
558
|
+
f"test -f /etc/systemd/system/{slot_service}.service && "
|
|
559
|
+
f"systemctl enable {slot_service} && "
|
|
560
|
+
f"systemctl restart {slot_service} || "
|
|
561
|
+
f"echo 'Unit {slot_service}.service not found, skipping'"
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
self.assertIn("test -f /etc/systemd/system/myapp-blue.service", cmd)
|
|
565
|
+
self.assertIn("systemctl enable myapp-blue", cmd)
|
|
566
|
+
self.assertIn("systemctl restart myapp-blue", cmd)
|
|
567
|
+
self.assertIn("not found, skipping", cmd)
|
|
568
|
+
|
|
569
|
+
def test_streaming_service_gets_separate_slot_name(self):
|
|
570
|
+
"""bostad-streaming should produce bostad-streaming-blue, not bostad-blue."""
|
|
571
|
+
from djaploy.infra.utils import get_slot_service_name
|
|
572
|
+
self.assertEqual(
|
|
573
|
+
get_slot_service_name("bostad-streaming", "blue"),
|
|
574
|
+
"bostad-streaming-blue",
|
|
575
|
+
)
|
|
576
|
+
self.assertEqual(
|
|
577
|
+
get_slot_service_name("bostad-streaming", "green"),
|
|
578
|
+
"bostad-streaming-green",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class TestHeredocInjectionSafety(unittest.TestCase):
|
|
583
|
+
"""Test that state.json update safely handles special characters."""
|
|
584
|
+
|
|
585
|
+
def test_release_name_with_quotes_is_safe(self):
|
|
586
|
+
"""json.dumps should escape quotes in release names."""
|
|
587
|
+
import json
|
|
588
|
+
release = "feat/o'hare"
|
|
589
|
+
safe = json.dumps(release)
|
|
590
|
+
self.assertEqual(safe, '"feat/o\'hare"')
|
|
591
|
+
# Should be parseable back
|
|
592
|
+
self.assertEqual(json.loads(safe), release)
|
|
593
|
+
|
|
594
|
+
def test_commit_with_special_chars_is_safe(self):
|
|
595
|
+
"""json.dumps should handle commit messages with special chars."""
|
|
596
|
+
import json
|
|
597
|
+
commit = 'fix: handle "edge" case'
|
|
598
|
+
safe = json.dumps(commit)
|
|
599
|
+
self.assertIn('\\"edge\\"', safe)
|
|
600
|
+
self.assertEqual(json.loads(safe), commit)
|
|
601
|
+
|
|
602
|
+
|
|
486
603
|
if __name__ == "__main__":
|
|
487
604
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|