gitops 1.6.0__tar.gz → 1.6.2__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 (87) hide show
  1. gitops-1.6.2/.release-please-manifest.json +5 -0
  2. {gitops-1.6.0 → gitops-1.6.2}/CHANGELOG.md +17 -0
  3. {gitops-1.6.0 → gitops-1.6.2}/PKG-INFO +10 -3
  4. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/CHANGELOG.md +9 -0
  5. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/Chart.yaml +1 -1
  6. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/templates/deployment.yml +2 -2
  7. {gitops-1.6.0 → gitops-1.6.2}/gitops/__init__.py +1 -1
  8. {gitops-1.6.0 → gitops-1.6.2}/gitops/utils/apps.py +4 -1
  9. gitops-1.6.2/gitops_server/version.py +1 -0
  10. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/workers/deployer/deploy.py +38 -12
  11. {gitops-1.6.0 → gitops-1.6.2}/pyproject.toml +18 -3
  12. {gitops-1.6.0 → gitops-1.6.2}/release-please-config.json +7 -0
  13. {gitops-1.6.0 → gitops-1.6.2}/tests/test_deploy.py +73 -0
  14. {gitops-1.6.0 → gitops-1.6.2}/tests/test_gitops_server.py +4 -2
  15. {gitops-1.6.0 → gitops-1.6.2}/uv.lock +301 -202
  16. gitops-1.6.0/.release-please-manifest.json +0 -5
  17. gitops-1.6.0/gitops_server/version.py +0 -1
  18. {gitops-1.6.0 → gitops-1.6.2}/.dockerignore +0 -0
  19. {gitops-1.6.0 → gitops-1.6.2}/.github/workflows/release-please.yaml +0 -0
  20. {gitops-1.6.0 → gitops-1.6.2}/.github/workflows/test.yml +0 -0
  21. {gitops-1.6.0 → gitops-1.6.2}/.gitignore +0 -0
  22. {gitops-1.6.0 → gitops-1.6.2}/.mise.toml +0 -0
  23. {gitops-1.6.0 → gitops-1.6.2}/.pre-commit-config.yaml +0 -0
  24. {gitops-1.6.0 → gitops-1.6.2}/CLAUDE.md +0 -0
  25. {gitops-1.6.0 → gitops-1.6.2}/Dockerfile +0 -0
  26. {gitops-1.6.0 → gitops-1.6.2}/LICENSE +0 -0
  27. {gitops-1.6.0 → gitops-1.6.2}/README.md +0 -0
  28. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/.helmignore +0 -0
  29. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/README.md +0 -0
  30. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/templates/_helpers.tpl +0 -0
  31. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/templates/certificate.yml +0 -0
  32. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/templates/environment.yml +0 -0
  33. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/templates/ingress.yml +0 -0
  34. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/templates/issuer.yml +0 -0
  35. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/templates/pdb.yml +0 -0
  36. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/templates/secrets.yml +0 -0
  37. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/templates/service.yml +0 -0
  38. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/templates/serviceaccount.yaml +0 -0
  39. {gitops-1.6.0 → gitops-1.6.2}/charts/gitops/values.yaml +0 -0
  40. {gitops-1.6.0 → gitops-1.6.2}/docker-compose.yml +0 -0
  41. {gitops-1.6.0 → gitops-1.6.2}/docs/plans/2026-03-13-kms-encryption-design.md +0 -0
  42. {gitops-1.6.0 → gitops-1.6.2}/gitops/common/__init__.py +0 -0
  43. {gitops-1.6.0 → gitops-1.6.2}/gitops/common/app.py +0 -0
  44. {gitops-1.6.0 → gitops-1.6.2}/gitops/common/utils.py +0 -0
  45. {gitops-1.6.0 → gitops-1.6.2}/gitops/core.py +0 -0
  46. {gitops-1.6.0 → gitops-1.6.2}/gitops/main.py +0 -0
  47. {gitops-1.6.0 → gitops-1.6.2}/gitops/py.typed +0 -0
  48. {gitops-1.6.0 → gitops-1.6.2}/gitops/settings.py +0 -0
  49. {gitops-1.6.0 → gitops-1.6.2}/gitops/shorthands.py +0 -0
  50. {gitops-1.6.0 → gitops-1.6.2}/gitops/utils/__init__.py +0 -0
  51. {gitops-1.6.0 → gitops-1.6.2}/gitops/utils/async_runner.py +0 -0
  52. {gitops-1.6.0 → gitops-1.6.2}/gitops/utils/cli.py +0 -0
  53. {gitops-1.6.0 → gitops-1.6.2}/gitops/utils/exceptions.py +0 -0
  54. {gitops-1.6.0 → gitops-1.6.2}/gitops/utils/images.py +0 -0
  55. {gitops-1.6.0 → gitops-1.6.2}/gitops/utils/kube.py +0 -0
  56. {gitops-1.6.0 → gitops-1.6.2}/gitops/utils/tags.py +0 -0
  57. {gitops-1.6.0 → gitops-1.6.2}/gitops/utils/yaml.py +0 -0
  58. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/CHANGELOG.md +0 -0
  59. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/README.md +0 -0
  60. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/__init__.py +0 -0
  61. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/app.py +0 -0
  62. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/main.py +0 -0
  63. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/settings.py +0 -0
  64. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/types.py +0 -0
  65. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/utils/__init__.py +0 -0
  66. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/utils/git.py +0 -0
  67. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/utils/github.py +0 -0
  68. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/utils/slack.py +0 -0
  69. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/workers/__init__.py +0 -0
  70. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/workers/deployer/__init__.py +0 -0
  71. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/workers/deployer/hooks.py +0 -0
  72. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/workers/deployer/semaphore_manager.py +0 -0
  73. {gitops-1.6.0 → gitops-1.6.2}/gitops_server/workers/deployer/worker.py +0 -0
  74. {gitops-1.6.0 → gitops-1.6.2}/release.py +0 -0
  75. {gitops-1.6.0 → gitops-1.6.2}/ruff.toml +0 -0
  76. {gitops-1.6.0 → gitops-1.6.2}/secrets.example.env +0 -0
  77. {gitops-1.6.0 → gitops-1.6.2}/setup.cfg +0 -0
  78. {gitops-1.6.0 → gitops-1.6.2}/tests/__init__.py +0 -0
  79. {gitops-1.6.0 → gitops-1.6.2}/tests/conftest.py +0 -0
  80. {gitops-1.6.0 → gitops-1.6.2}/tests/sample_data.py +0 -0
  81. {gitops-1.6.0 → gitops-1.6.2}/tests/test_app.py +0 -0
  82. {gitops-1.6.0 → gitops-1.6.2}/tests/test_core.py +0 -0
  83. {gitops-1.6.0 → gitops-1.6.2}/tests/test_deploy_concurrent.py +0 -0
  84. {gitops-1.6.0 → gitops-1.6.2}/tests/test_git.py +0 -0
  85. {gitops-1.6.0 → gitops-1.6.2}/tests/test_utils.py +0 -0
  86. {gitops-1.6.0 → gitops-1.6.2}/tests/utils.py +0 -0
  87. {gitops-1.6.0 → gitops-1.6.2}/tests/webhook_sample_data.py +0 -0
@@ -0,0 +1,5 @@
1
+ {
2
+ ".": "1.6.2",
3
+ "gitops_server": "0.15.1",
4
+ "charts/gitops": "1.2.2"
5
+ }
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.2](https://github.com/uptick/gitops/compare/gitops-v1.6.1...gitops-v1.6.2) (2026-06-23)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **deploy:** isolate per-app failures so one bad app doesn't abort the batch ([29ff9c3](https://github.com/uptick/gitops/commit/29ff9c378ab8d003fc862f274b39bc131ada5871))
9
+ * **deploy:** isolate per-app failures so one bad app doesn't abort the batch ([8261b7b](https://github.com/uptick/gitops/commit/8261b7bb335678a029586b2238e81cb783988009))
10
+ * **deploy:** make post_result fail-proof so reporting errors don't abort the batch ([83dbc8e](https://github.com/uptick/gitops/commit/83dbc8e217e358ea51168eb955007ac76bceb9aa))
11
+
12
+ ## [1.6.1](https://github.com/uptick/gitops/compare/gitops-v1.6.0...gitops-v1.6.1) (2026-05-28)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * improve app not found error message ([2c2a5a6](https://github.com/uptick/gitops/commit/2c2a5a6398ec6f7e7609982e6105b87cc6fd0d9d))
18
+ * improve app not found error message ([e0f93b4](https://github.com/uptick/gitops/commit/e0f93b40f14c732ac1661c6a61e981eee05e32b2))
19
+
3
20
  ## [1.6.0](https://github.com/uptick/gitops/compare/gitops-v1.5.1...gitops-v1.6.0) (2026-05-06)
4
21
 
5
22
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitops
3
- Version: 1.6.0
3
+ Version: 1.6.2
4
4
  Summary: Manage multiple apps across one or more k8s clusters.
5
5
  Author-email: Jarek Głowacki <jarekwg@gmail.com>
6
6
  License-File: LICENSE
@@ -13,12 +13,19 @@ Requires-Dist: invoke>=2.2.0
13
13
  Requires-Dist: packaging>=24.1
14
14
  Requires-Dist: pyyaml>=6.0.2
15
15
  Requires-Dist: tabulate>=0.8.9
16
+ Requires-Dist: urllib3>=2.7.0
16
17
  Provides-Extra: server
17
- Requires-Dist: fastapi==0.109.2; extra == 'server'
18
- Requires-Dist: httpx<0.24.0,>=0.23.0; extra == 'server'
18
+ Requires-Dist: aiohttp>=3.14.0; extra == 'server'
19
+ Requires-Dist: fastapi>=0.133.0; extra == 'server'
20
+ Requires-Dist: h11>=0.16.0; extra == 'server'
21
+ Requires-Dist: httpx>=0.27.0; extra == 'server'
22
+ Requires-Dist: idna>=3.15; extra == 'server'
19
23
  Requires-Dist: kubernetes-asyncio>=25.1.2; extra == 'server'
24
+ Requires-Dist: protobuf>=5.29.6; extra == 'server'
25
+ Requires-Dist: requests>=2.33.0; extra == 'server'
20
26
  Requires-Dist: sentry-sdk>=1.3.0; extra == 'server'
21
27
  Requires-Dist: slack-sdk>=3.33.5; extra == 'server'
28
+ Requires-Dist: starlette>=1.3.1; extra == 'server'
22
29
  Requires-Dist: uptick-observability[fastapi]>=0.2.2; extra == 'server'
23
30
  Requires-Dist: uvicorn>=0.34.0; extra == 'server'
24
31
  Description-Content-Type: text/markdown
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.2](https://github.com/uptick/gitops/compare/helm-v1.2.1...helm-v1.2.2) (2026-05-28)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **helm:** remove trailing colon from OTEL_RESOURCE_ATTRIBUTES env var name ([0c23326](https://github.com/uptick/gitops/commit/0c23326d62ed331e5ca24b9c3a04bbc3a58b2931))
9
+ * **helm:** remove trailing colon from OTEL_RESOURCE_ATTRIBUTES env var name ([f454c45](https://github.com/uptick/gitops/commit/f454c4587cd79fda5ac8ad30fd9a65541f0f4636))
10
+ * **helm:** use correct values reference for OTEL_RESOURCE_ATTRIBUTES cluster name ([2184d70](https://github.com/uptick/gitops/commit/2184d7040c36b802af7c86a1b6c074c92940b269))
11
+
3
12
  ## [1.2.1](https://github.com/uptick/gitops/compare/helm-v1.2.0...helm-v1.2.1) (2026-05-06)
4
13
 
5
14
 
@@ -2,4 +2,4 @@ apiVersion: v1
2
2
  appVersion: "1.0"
3
3
  description: GitOps Server Helm chart.
4
4
  name: gitops
5
- version: 1.2.1
5
+ version: 1.2.2
@@ -90,8 +90,8 @@ spec:
90
90
  value: "gitops"
91
91
  - name: OTEL_EXPORTER_OTLP_TIMEOUT
92
92
  value: "1"
93
- - name: OTEL_RESOURCE_ATTRIBUTES:
94
- value: "cluster={{.Values.cluster}}"
93
+ - name: OTEL_RESOURCE_ATTRIBUTES
94
+ value: "cluster={{.Values.environment.CLUSTER_NAME}}"
95
95
  # We want to limit the length of the attributes so that we don't store too much
96
96
  - name: OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT
97
97
  value: "512"
@@ -6,7 +6,7 @@ from gitops.utils.apps import App, get_app_details, get_apps # type: ignore[att
6
6
 
7
7
  from .utils.cli import success, warning
8
8
 
9
- __version__ = "1.6.0"
9
+ __version__ = "1.6.2"
10
10
 
11
11
 
12
12
  # Checking gitops version matches cluster repo version.
@@ -38,7 +38,10 @@ def get_app_details(
38
38
  except FileNotFoundError as e:
39
39
  msg, exc = "", Exception
40
40
  if get_apps_directory().exists():
41
- msg, exc = f"There's no app with the name '{app_name}', silly.", AppDoesNotExist
41
+ msg, exc = (
42
+ f"There's no app with the name '{app_name}', silly. is your cluster repo up to date?",
43
+ AppDoesNotExist,
44
+ )
42
45
  else:
43
46
  msg, exc = "Could not find an 'apps' directory. Are you in a cluster repo?", AppDirectoryDoesNotExist
44
47
  if exit_if_not_found:
@@ -0,0 +1 @@
1
+ __version__ = "1.6.2"
@@ -162,9 +162,27 @@ class Deployer:
162
162
  )
163
163
 
164
164
  async def update_app_deployment(self, app: App) -> UpdateAppResult | None:
165
+ from_timestamp = time.time()
165
166
  async with self.semaphore_manager.app_semaphore(app.name):
166
167
  async with helm_parallel_semaphore:
167
- return await self._update_app_deployment(app)
168
+ try:
169
+ return await self._update_app_deployment(app)
170
+ except Exception as e:
171
+ # Isolate per-app failures so one broken app doesn't abort the
172
+ # whole batch. Steps before the helm upgrade (e.g. cloning a
173
+ # custom git chart from a branch that doesn't exist, or
174
+ # `helm dependency build`) raise rather than returning a
175
+ # non-zero result, and would otherwise propagate up through
176
+ # asyncio.gather() and cancel every other app's deployment.
177
+ logger.exception(f"Failed to deploy app {app.name!r}.")
178
+ result = UpdateAppResult(
179
+ app_name=app.name,
180
+ slack_message="",
181
+ exit_code=1,
182
+ output=str(e),
183
+ )
184
+ await self.post_result(app=app, result=result, deployer=self, from_timestamp=from_timestamp)
185
+ return result
168
186
 
169
187
  async def _update_app_deployment(self, app: App) -> UpdateAppResult | None:
170
188
  app.set_value("deployment.labels.gitops/deploy_id", self.deploy_id)
@@ -275,17 +293,25 @@ class Deployer:
275
293
 
276
294
  @tracer.start_as_current_span("post_result")
277
295
  async def post_result(self, app: App, result: UpdateAppResult, deployer: "Deployer", **kwargs: Any) -> None:
278
- if result["exit_code"] != 0:
279
- self.failed_apps.add(app.name)
280
- deploy_result = await handle_failed_deploy(app, result, deployer, **kwargs)
281
- message = (
282
- deploy_result["slack_message"]
283
- or f"Failed to deploy app `{result['app_name']}` for cluster `{settings.CLUSTER_NAME}`:\n>>>{result['output']}"
284
- )
285
- await slack.post_alert(message)
286
- else:
287
- self.successful_apps.add(app.name)
288
- await handle_successful_deploy(app, result, deployer)
296
+ # Reporting a result makes Slack/GitHub network calls and runs inside the
297
+ # asyncio.gather() in deploy(). A failure here (e.g. a Slack or GitHub API
298
+ # error) must not propagate, or it would cancel every other app's
299
+ # deployment in the batch. The success/failure bookkeeping is recorded
300
+ # before any network call so it survives a notification failure.
301
+ try:
302
+ if result["exit_code"] != 0:
303
+ self.failed_apps.add(app.name)
304
+ deploy_result = await handle_failed_deploy(app, result, deployer, **kwargs)
305
+ message = (
306
+ deploy_result["slack_message"]
307
+ or f"Failed to deploy app `{result['app_name']}` for cluster `{settings.CLUSTER_NAME}`:\n>>>{result['output']}"
308
+ )
309
+ await slack.post_alert(message)
310
+ else:
311
+ self.successful_apps.add(app.name)
312
+ await handle_successful_deploy(app, result, deployer)
313
+ except Exception:
314
+ logger.exception(f"Failed to post deploy result for app {app.name!r}.")
289
315
 
290
316
  # TODO: Fix this in the future. We want to update the message with the latest status.
291
317
  # But we can't because of the messge length (when messaging > 4000 characters)
@@ -1,7 +1,7 @@
1
1
 
2
2
  [project]
3
3
  name = "gitops"
4
- version = "1.6.0"
4
+ version = "1.6.2"
5
5
  description = "Manage multiple apps across one or more k8s clusters."
6
6
  requires-python = ">=3.12"
7
7
  readme = "README.md"
@@ -15,6 +15,8 @@ dependencies = [
15
15
  "humanize>=3.5.0",
16
16
  "tabulate>=0.8.9",
17
17
  "packaging>=24.1",
18
+ # Transitive security pins
19
+ "urllib3>=2.7.0",
18
20
  ]
19
21
 
20
22
  [project.scripts]
@@ -23,13 +25,20 @@ gitops = "gitops.main:program.run"
23
25
 
24
26
  [project.optional-dependencies]
25
27
  server = [
26
- "httpx>=0.23.0,<0.24.0",
28
+ "httpx>=0.27.0",
27
29
  "uvicorn>=0.34.0",
28
- "fastapi==0.109.2",
30
+ "fastapi>=0.133.0",
29
31
  "kubernetes_asyncio>=25.1.2",
30
32
  "sentry-sdk>=1.3.0",
31
33
  "uptick-observability[fastapi]>=0.2.2",
32
34
  "slack-sdk>=3.33.5",
35
+ # Transitive security pins
36
+ "h11>=0.16.0",
37
+ "aiohttp>=3.14.0",
38
+ "starlette>=1.3.1",
39
+ "protobuf>=5.29.6",
40
+ "requests>=2.33.0",
41
+ "idna>=3.15",
33
42
  ]
34
43
 
35
44
  [build-system]
@@ -51,6 +60,12 @@ dev-dependencies = [
51
60
 
52
61
 
53
62
  [tool.uv.sources]
63
+ uptick-observability = { index = "uptick" }
64
+
65
+ [[tool.uv.index]]
66
+ name = "uptick"
67
+ url = "https://pypi.deployments.onuptick.com"
68
+ explicit = true
54
69
 
55
70
  [tool.nitpick]
56
71
  # Generated by the 'nitpick init' command
@@ -7,6 +7,13 @@
7
7
  "exclude-paths": [
8
8
  "tests/",
9
9
  "charts/gitops"
10
+ ],
11
+ "extra-files": [
12
+ {
13
+ "type": "toml",
14
+ "path": "uv.lock",
15
+ "jsonpath": "$.package[?(@.name.value=='gitops')].version"
16
+ }
10
17
  ]
11
18
  },
12
19
  "charts/gitops": {
@@ -145,6 +145,79 @@ class TestDeploy:
145
145
  )
146
146
  assert post_mock.call_count == 2
147
147
 
148
+ @patch("gitops_server.workers.deployer.deploy.run")
149
+ @patch("gitops_server.utils.slack.post")
150
+ @patch("gitops_server.workers.deployer.deploy.Deployer.post_result")
151
+ @patch("gitops_server.workers.deployer.deploy.load_app_definitions", mock_load_app_definitions)
152
+ @patch("gitops_server.workers.deployer.deploy.temp_repo")
153
+ async def test_one_app_failing_does_not_abort_the_batch(
154
+ self, temp_repo_mock, post_result_mock, post_mock, run_mock
155
+ ):
156
+ """A single app failing (e.g. cloning a custom chart from a branch that
157
+ doesn't exist) must not prevent the rest of the batch from deploying."""
158
+ run_mock.return_value = {"exit_code": 0, "output": ""}
159
+
160
+ class _FakeTempRepo:
161
+ async def __aenter__(self):
162
+ return "mock-repo"
163
+
164
+ async def __aexit__(self, *args):
165
+ return False
166
+
167
+ # SAMPLE_GITHUB_PAYLOAD deploys two apps. Make the first chart fetch blow
168
+ # up (as a non-existent git branch would), the second succeed.
169
+ calls = {"n": 0}
170
+
171
+ def temp_repo_side_effect(*args, **kwargs):
172
+ calls["n"] += 1
173
+ if calls["n"] == 1:
174
+ raise Exception("git clone failed: branch 'does-not-exist' not found")
175
+ return _FakeTempRepo()
176
+
177
+ temp_repo_mock.side_effect = temp_repo_side_effect
178
+
179
+ semaphore_manager = AppSemaphoreManager()
180
+ deployer = await Deployer.from_push_event(SAMPLE_GITHUB_PAYLOAD, semaphore_manager)
181
+
182
+ # Must not raise even though one app errors out.
183
+ await deployer.deploy()
184
+
185
+ # The surviving app still ran its full helm flow (dependency build + upgrade).
186
+ assert run_mock.call_count == 2
187
+ # post_result was called for both apps: one failure, one success.
188
+ assert post_result_mock.call_count == 2
189
+ exit_codes = sorted(call.kwargs["result"]["exit_code"] for call in post_result_mock.call_args_list)
190
+ assert exit_codes == [0, 1]
191
+
192
+ @patch("gitops_server.workers.deployer.deploy.handle_failed_deploy")
193
+ @patch("gitops_server.workers.deployer.deploy.handle_successful_deploy")
194
+ @patch("gitops_server.workers.deployer.deploy.load_app_definitions", mock_load_app_definitions)
195
+ async def test_post_result_swallows_reporting_failures(self, handle_success_mock, handle_failed_mock):
196
+ """Reporting a result makes Slack/GitHub network calls inside the gather()
197
+ in deploy(); a failure there must never propagate, or it would abort the
198
+ rest of the batch. post_result must swallow such errors itself, since it
199
+ is also invoked from places not wrapped by update_app_deployment's guard
200
+ (its own except handler and uninstall_app)."""
201
+ handle_success_mock.side_effect = Exception("slack/github exploded")
202
+ handle_failed_mock.side_effect = Exception("slack/github exploded")
203
+
204
+ semaphore_manager = AppSemaphoreManager()
205
+ deployer = await Deployer.from_push_event(SAMPLE_GITHUB_PAYLOAD, semaphore_manager)
206
+ app = App("some-app", path=create_test_yaml())
207
+
208
+ # Neither the success nor the failure reporting path may raise...
209
+ await deployer.post_result(
210
+ app, {"app_name": app.name, "slack_message": "", "exit_code": 0, "output": ""}, deployer
211
+ )
212
+ await deployer.post_result(
213
+ app, {"app_name": app.name, "slack_message": "", "exit_code": 1, "output": "boom"}, deployer
214
+ )
215
+
216
+ # ...but the success/failure bookkeeping is still recorded (it happens
217
+ # before the network call that blows up).
218
+ assert "some-app" in deployer.successful_apps
219
+ assert "some-app" in deployer.failed_apps
220
+
148
221
  @patch("gitops_server.workers.deployer.deploy.run")
149
222
  @patch("gitops_server.utils.slack.post")
150
223
  @patch("gitops_server.workers.deployer.deploy.load_app_definitions", mock_load_app_definitions)
@@ -22,12 +22,14 @@ def test_get_on_index_returns_200():
22
22
 
23
23
  @patch("gitops_server.main.settings.GITHUB_WEBHOOK_KEY", "test_key")
24
24
  def test_webhook_returns_200_if_hmac_is_correct():
25
- sha_encoding = hmac.new(b"test_key", json.dumps(payload).encode(), hashlib.sha1).hexdigest()
25
+ body = json.dumps(payload).encode()
26
+ sha_encoding = hmac.new(b"test_key", body, hashlib.sha1).hexdigest()
26
27
  headers["X-Hub-Signature"] = f"sha1={sha_encoding}"
28
+ headers["Content-Type"] = "application/json"
27
29
 
28
30
  with patch.object(gitops_server.workers.DeployQueueWorker, "get_worker") as get_worker_mock:
29
31
  get_worker_mock.return_value = AsyncMock()
30
- response = client.post("/webhook", headers=headers, json=payload)
32
+ response = client.post("/webhook", headers=headers, content=body)
31
33
 
32
34
  assert response.status_code == 200
33
35
  get_worker_mock.assert_called()