djaploy 1.2.3__tar.gz → 1.2.5__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.
Files changed (91) hide show
  1. {djaploy-1.2.3 → djaploy-1.2.5}/PKG-INFO +1 -1
  2. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/borg/infra/djaploy_hooks.py +24 -2
  3. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/commands/rollback.py +10 -0
  4. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/infra/djaploy_hooks.py +85 -65
  5. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy.egg-info/PKG-INFO +1 -1
  6. {djaploy-1.2.3 → djaploy-1.2.5}/pyproject.toml +1 -1
  7. {djaploy-1.2.3 → djaploy-1.2.5}/LICENSE +0 -0
  8. {djaploy-1.2.3 → djaploy-1.2.5}/README.md +0 -0
  9. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/__init__.py +0 -0
  10. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/app.py +0 -0
  11. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/__init__.py +0 -0
  12. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/borg/__init__.py +0 -0
  13. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/borg/infra/__init__.py +0 -0
  14. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/janitor/__init__.py +0 -0
  15. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/janitor/apps.py +0 -0
  16. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/janitor/infra/__init__.py +0 -0
  17. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/janitor/infra/commands/createjanitoruser.py +0 -0
  18. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/janitor/infra/djaploy_hooks.py +0 -0
  19. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/nginx/__init__.py +0 -0
  20. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/nginx/apps.py +0 -0
  21. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/nginx/infra/__init__.py +0 -0
  22. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/nginx/infra/djaploy_hooks.py +0 -0
  23. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/rclone/__init__.py +0 -0
  24. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/rclone/apps.py +0 -0
  25. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/rclone/infra/__init__.py +0 -0
  26. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/rclone/infra/djaploy_hooks.py +0 -0
  27. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/sync_certs/__init__.py +0 -0
  28. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/sync_certs/apps.py +0 -0
  29. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/sync_certs/infra/__init__.py +0 -0
  30. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/sync_certs/infra/djaploy_hooks.py +0 -0
  31. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/systemd/__init__.py +0 -0
  32. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/systemd/apps.py +0 -0
  33. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/systemd/infra/__init__.py +0 -0
  34. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/systemd/infra/djaploy_hooks.py +0 -0
  35. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/tailscale/__init__.py +0 -0
  36. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/tailscale/apps.py +0 -0
  37. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/tailscale/infra/__init__.py +0 -0
  38. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/tailscale/infra/djaploy_hooks.py +0 -0
  39. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/versioning/__init__.py +0 -0
  40. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/versioning/apps.py +0 -0
  41. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/versioning/infra/__init__.py +0 -0
  42. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/apps/versioning/infra/djaploy_hooks.py +0 -0
  43. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/artifact.py +0 -0
  44. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/bin/__init__.py +0 -0
  45. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/bin/django_pyinfra.py +0 -0
  46. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/bin/gunicornherder.py +0 -0
  47. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/builtin_hooks.py +0 -0
  48. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/certificates.py +0 -0
  49. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/changelog.py +0 -0
  50. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/commands/__init__.py +0 -0
  51. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/commands/_utils.py +0 -0
  52. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/commands/activate.py +0 -0
  53. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/commands/configure.py +0 -0
  54. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/commands/deploy.py +0 -0
  55. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/commands/restore.py +0 -0
  56. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/commands/status.py +0 -0
  57. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/commands/sync_certs.py +0 -0
  58. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/config.py +0 -0
  59. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/deploy.py +0 -0
  60. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/discovery.py +0 -0
  61. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/hooks.py +0 -0
  62. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/infra/__init__.py +0 -0
  63. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/infra/bluegreen.py +0 -0
  64. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/infra/templates.py +0 -0
  65. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/infra/utils.py +0 -0
  66. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/management/__init__.py +0 -0
  67. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/management/commands/__init__.py +0 -0
  68. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/management/commands/djaploy.py +0 -0
  69. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/management/commands/restore_backup.py +0 -0
  70. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/management/commands/sync_certs.py +0 -0
  71. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/management/commands/update_certs.py +0 -0
  72. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/management/commands/verify.py +0 -0
  73. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/management/utils.py +0 -0
  74. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/notifications.py +0 -0
  75. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/utils.py +0 -0
  76. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/version.py +0 -0
  77. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy/versioning.py +0 -0
  78. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy.egg-info/SOURCES.txt +0 -0
  79. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy.egg-info/dependency_links.txt +0 -0
  80. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy.egg-info/entry_points.txt +0 -0
  81. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy.egg-info/requires.txt +0 -0
  82. {djaploy-1.2.3 → djaploy-1.2.5}/djaploy.egg-info/top_level.txt +0 -0
  83. {djaploy-1.2.3 → djaploy-1.2.5}/setup.cfg +0 -0
  84. {djaploy-1.2.3 → djaploy-1.2.5}/tests/test_bluegreen.py +0 -0
  85. {djaploy-1.2.3 → djaploy-1.2.5}/tests/test_borg.py +0 -0
  86. {djaploy-1.2.3 → djaploy-1.2.5}/tests/test_config.py +0 -0
  87. {djaploy-1.2.3 → djaploy-1.2.5}/tests/test_deploy_scripts.py +0 -0
  88. {djaploy-1.2.3 → djaploy-1.2.5}/tests/test_discovery.py +0 -0
  89. {djaploy-1.2.3 → djaploy-1.2.5}/tests/test_gunicornherder.py +0 -0
  90. {djaploy-1.2.3 → djaploy-1.2.5}/tests/test_hooks.py +0 -0
  91. {djaploy-1.2.3 → djaploy-1.2.5}/tests/test_versioning.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djaploy
3
- Version: 1.2.3
3
+ Version: 1.2.5
4
4
  Summary: Modular Django deployment system based on pyinfra
5
5
  Author-email: Johanna Mae Dimayuga <johanna@techco.fi>
6
6
  License: MIT License
@@ -146,7 +146,7 @@ def _generate_backup_script(borg_config, app_user: str, repo_name: str,
146
146
  if not isinstance(host_data, dict)
147
147
  else host_data.get('deployment_strategy', 'zero_downtime')
148
148
  )
149
- if deployment_strategy == 'zero_downtime':
149
+ if deployment_strategy in ('zero_downtime', 'bluegreen'):
150
150
  default_media = f"/home/{app_user}/apps/{app_name}/shared/media"
151
151
  else:
152
152
  default_media = f"/home/{app_user}/apps/{app_name}/media"
@@ -527,7 +527,7 @@ def restore_borg(host_data, restore_opts):
527
527
  if not isinstance(host_data, dict)
528
528
  else host_data.get('deployment_strategy', 'zero_downtime')
529
529
  )
530
- if deployment_strategy == 'zero_downtime':
530
+ if deployment_strategy in ('zero_downtime', 'bluegreen'):
531
531
  default_media = f"/home/{app_user}/apps/{app_name}/shared/media"
532
532
  else:
533
533
  default_media = f"/home/{app_user}/apps/{app_name}/media"
@@ -540,6 +540,28 @@ def restore_borg(host_data, restore_opts):
540
540
  db_only = restore_opts.get("db_only", False)
541
541
  services = getattr(host_data, "services", None) or []
542
542
 
543
+ # For bluegreen, expand service names to per-slot services and include
544
+ # per-slot streaming services so they are also stopped during restore.
545
+ deployment_strategy = (
546
+ getattr(host_data, 'deployment_strategy', 'zero_downtime')
547
+ if not isinstance(host_data, dict)
548
+ else host_data.get('deployment_strategy', 'zero_downtime')
549
+ )
550
+ if deployment_strategy == 'bluegreen':
551
+ app_name = (
552
+ getattr(host_data, 'app_name', '')
553
+ if not isinstance(host_data, dict)
554
+ else host_data.get('app_name', '')
555
+ )
556
+ bluegreen_services = []
557
+ for svc in services:
558
+ bluegreen_services.append(f"{app_name}-blue")
559
+ bluegreen_services.append(f"{app_name}-green")
560
+ # Also stop streaming coordinator instances
561
+ bluegreen_services.append(f"{app_name}-streaming-blue")
562
+ bluegreen_services.append(f"{app_name}-streaming-green")
563
+ services = bluegreen_services
564
+
543
565
  # Repo connection details come from the *source* borg config, but
544
566
  # ssh_known_hosts_file must come from the *target* (the file lives on
545
567
  # the machine running borg, not the machine the backup was taken from).
@@ -24,3 +24,13 @@ for hook in registry.get_remote_hooks("rollback"):
24
24
  _deploy_decorator(hook.function.__name__)(hook.function)(
25
25
  host.data, release
26
26
  )
27
+
28
+ # For bluegreen rollback, run activate:post hooks to update
29
+ # custom nginx upstream, timers, etc.
30
+ from djaploy.infra.utils import is_bluegreen
31
+ if is_bluegreen(host.data):
32
+ for phase in ("activate:post",):
33
+ for hook in registry.get_remote_hooks(phase):
34
+ _deploy_decorator(hook.function.__name__)(hook.function)(
35
+ host.data
36
+ )
@@ -160,6 +160,7 @@ def configure_server(host_data):
160
160
  _sudo_user=app_user,
161
161
  )
162
162
 
163
+
163
164
  shared_resources = getattr(host_data, 'shared_resources', None) or []
164
165
  if shared_resources:
165
166
  mkdir_commands = [
@@ -712,61 +713,74 @@ def rollback_release(host_data, release=None):
712
713
  app_path = get_app_path(host_data)
713
714
 
714
715
  if is_bluegreen(host_data):
716
+ from pyinfra import host
717
+ from pyinfra.facts.server import Command
715
718
  from djaploy.infra.bluegreen import (
716
719
  read_active_slot_cmd, set_active_slot_cmd, other_slot,
717
720
  )
718
721
  from djaploy.infra.utils import get_slot_socket_path
719
722
  from io import StringIO
720
723
  from djaploy.infra.templates import NGINX_UPSTREAM_BLUEGREEN, build_template_context
724
+ from jinja2 import Environment
721
725
 
722
726
  app_name = getattr(host_data, 'app_name', None)
723
727
  state_file = f"{app_path}/state.json"
724
728
 
725
- # Read active slot and switch to the other one
729
+ # Read active slot and compute target
730
+ active_slot = host.get_fact(Command, read_active_slot_cmd(state_file))
731
+ active_slot = (active_slot or "").strip()
732
+ target_slot = other_slot(active_slot) if active_slot else "blue"
733
+
734
+ # Store for activate:post hooks (timers, streaming, custom nginx)
735
+ host.data._bluegreen_activated_slot = target_slot
736
+
737
+ # Update nginx upstream for non-custom setups
738
+ ctx = build_template_context(host_data)
739
+ ctx["active_slot"] = target_slot
740
+ upstream_content = Environment().from_string(NGINX_UPSTREAM_BLUEGREEN).render(**ctx)
741
+
742
+ nginx_cfg = getattr(host_data, 'nginx_conf', None) or {}
743
+ if not nginx_cfg.get("custom"):
744
+ from pyinfra.operations import files
745
+ files.put(
746
+ name=f"Update nginx upstream to {target_slot} slot",
747
+ src=StringIO(upstream_content),
748
+ dest=f"/etc/nginx/sites-available/{app_name}-upstream.conf",
749
+ _sudo=True,
750
+ )
751
+
752
+ # Reload nginx
726
753
  server.shell(
727
- name="Roll back blue-green: switch to other slot",
728
- commands=[
729
- # Read current active slot, compute target, verify it has a deployment
730
- f'ACTIVE=$({read_active_slot_cmd(state_file)}) && '
731
- f'if [ "$ACTIVE" = "blue" ]; then TARGET=green; else TARGET=blue; fi && '
732
- f'echo "Rolling back: switching from $ACTIVE to $TARGET"',
733
- ],
754
+ name="Test and reload nginx for rollback",
755
+ commands=["nginx -t && nginx -s reload"],
756
+ _sudo=True,
757
+ )
758
+
759
+ # Update state.json
760
+ server.shell(
761
+ name=f"Set active slot to {target_slot} (rollback)",
762
+ commands=[set_active_slot_cmd(state_file, target_slot)],
734
763
  _sudo=True,
735
764
  _sudo_user=app_user,
736
765
  )
737
766
 
738
- # Update nginx upstream to the other slot and reload
739
- # We render both variants and use a shell command to pick the right one
740
- for target_slot in ("blue", "green"):
741
- ctx = build_template_context(host_data)
742
- ctx["active_slot"] = target_slot
743
- from jinja2 import Environment
744
- upstream_content = Environment().from_string(NGINX_UPSTREAM_BLUEGREEN).render(**ctx)
767
+ # Print summary
768
+ from pyinfra.operations import python as python_op
745
769
 
746
- server.shell(
747
- name=f"Prepare nginx upstream for {target_slot}",
748
- commands=[
749
- f'ACTIVE=$({read_active_slot_cmd(state_file)}) && '
750
- f'if [ "$ACTIVE" = "blue" ]; then TARGET=green; else TARGET=blue; fi && '
751
- f'if [ "$TARGET" = "{target_slot}" ]; then '
752
- f"cat > /etc/nginx/sites-available/{app_name}-upstream.conf << 'NGINX_EOF'\n{upstream_content}NGINX_EOF\n"
753
- f'fi',
754
- ],
755
- _sudo=True,
756
- )
770
+ def _print_rollback_summary(state_f, t_slot, p_slot):
771
+ info = _read_slot_info_from_remote(host, t_slot, state_f)
772
+ print(f"\n=== Blue-Green Rollback ===")
773
+ print(f" Switched: {p_slot} -> {t_slot}")
774
+ print(f" Release: {info.get('release', 'unknown')}")
775
+ print(f" Python: {info.get('python_interpreter', 'unknown')}")
776
+ print()
757
777
 
758
- server.shell(
759
- name="Reload nginx and update state for rollback",
760
- commands=[
761
- f'nginx -t && nginx -s reload',
762
- f'ACTIVE=$({read_active_slot_cmd(state_file)}) && '
763
- f'if [ "$ACTIVE" = "blue" ]; then TARGET=green; else TARGET=blue; fi && '
764
- + set_active_slot_cmd(state_file, '" + "$TARGET" + "').replace(
765
- "'" + '"' + " + " + '"' + "$TARGET" + '"' + " + " + '"' + "'",
766
- "'\"$TARGET\"'"
767
- ),
768
- ],
769
- _sudo=True,
778
+ python_op.call(
779
+ name="Print rollback summary",
780
+ function=_print_rollback_summary,
781
+ state_f=state_file,
782
+ t_slot=target_slot,
783
+ p_slot=active_slot or "none",
770
784
  )
771
785
  return
772
786
 
@@ -822,37 +836,41 @@ def activate_bluegreen(host_data):
822
836
  app_path = get_app_path(host_data)
823
837
  state_file = f"{app_path}/state.json"
824
838
 
825
- # Read current active slot
826
- active_slot = host.get_fact(
827
- Command,
828
- read_active_slot_cmd(state_file),
829
- )
830
- active_slot = (active_slot or "").strip()
831
- target_slot = other_slot(active_slot) if active_slot else "blue"
832
-
833
- # Verify target slot has a deployment
834
- has_deploy = host.get_fact(
835
- Command,
836
- f"python3 -c \""
837
- f"import json; s=json.load(open('{state_file}')); "
838
- f"print('yes' if s['slots'].get('{target_slot}') else 'no')"
839
- f"\"",
840
- )
841
- if (has_deploy or "").strip() != "yes":
842
- # Already on the latest slot — nothing to activate.
843
- # Print a message but don't fail the command.
839
+ # If called from deploy --activate, the target slot is already known
840
+ # and we can skip the stale-fact verification.
841
+ deploy_target = getattr(host.data, '_bluegreen_target_slot', None)
842
+ if deploy_target:
843
+ # deploy --activate: we just deployed to this slot, activate it
844
+ active_slot_raw = host.get_fact(
845
+ Command,
846
+ read_active_slot_cmd(state_file),
847
+ )
848
+ active_slot = (active_slot_raw or "").strip()
849
+ target_slot = deploy_target
850
+ else:
851
+ # Standalone activate: read state and switch to the other slot
852
+ active_slot_raw = host.get_fact(
853
+ Command,
854
+ read_active_slot_cmd(state_file),
855
+ )
856
+ active_slot = (active_slot_raw or "").strip()
857
+ target_slot = other_slot(active_slot) if active_slot else "blue"
858
+
859
+ # Verify target slot has a deployment (use server.shell to avoid
860
+ # fact caching issues — the check runs at execution time, not plan time)
861
+ current_display = active_slot or "none"
844
862
  server.shell(
845
- name="Nothing to activate",
863
+ name=f"Verify {target_slot} slot has a deployment",
846
864
  commands=[
847
- f'echo ""',
848
- f'echo "Nothing to activate: slot {target_slot} has no deployment."',
849
- f'echo "Current active slot: {active_slot or "none"}"',
850
- f'echo "Run \'djaploy deploy\' first to stage a new version."',
851
- f'echo ""',
865
+ f"python3 -c \""
866
+ f"import json, sys; s=json.load(open('{state_file}')); "
867
+ f"sys.exit(0 if s['slots'].get('{target_slot}') else 1)"
868
+ f"\" || (echo 'Nothing to activate: slot {target_slot} has no deployment.' && "
869
+ f"echo 'Current active slot: {current_display}' && "
870
+ f"echo 'Run djaploy deploy first to stage a new version.' && exit 1)",
852
871
  ],
853
872
  _sudo=True,
854
873
  )
855
- return
856
874
 
857
875
  # Store on host.data so activate:post hooks can read it
858
876
  host.data._bluegreen_activated_slot = target_slot
@@ -866,9 +884,11 @@ def activate_bluegreen(host_data):
866
884
  ctx["active_slot"] = target_slot
867
885
  upstream_content = Environment().from_string(NGINX_UPSTREAM_BLUEGREEN).render(**ctx)
868
886
 
887
+ # Write the upstream config. Custom nginx setups (e.g. bostad) handle
888
+ # their own upstream file via activate:post hooks.
889
+ from pyinfra.operations import files
869
890
  nginx_cfg = getattr(host_data, 'nginx_conf', None) or {}
870
891
  if not nginx_cfg.get("custom"):
871
- from pyinfra.operations import files
872
892
  files.put(
873
893
  name=f"Update nginx upstream to {target_slot} slot",
874
894
  src=StringIO(upstream_content),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djaploy
3
- Version: 1.2.3
3
+ Version: 1.2.5
4
4
  Summary: Modular Django deployment system based on pyinfra
5
5
  Author-email: Johanna Mae Dimayuga <johanna@techco.fi>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "djaploy"
3
- version = "1.2.3"
3
+ version = "1.2.5"
4
4
  description = "Modular Django deployment system based on pyinfra"
5
5
  authors = [
6
6
  {name = "Johanna Mae Dimayuga", email = "johanna@techco.fi"}
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