imbi-plugin-github 1.3.0__tar.gz → 1.4.0__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 (22) hide show
  1. imbi_plugin_github-1.4.0/CLAUDE.md +128 -0
  2. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/PKG-INFO +5 -5
  3. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/README.md +4 -4
  4. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/pyproject.toml +3 -1
  5. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/src/imbi_plugin_github/deployment.py +1 -1
  6. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/src/imbi_plugin_github/identity.py +2 -3
  7. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/src/imbi_plugin_github/lifecycle.py +53 -2
  8. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/tests/test_lifecycle.py +122 -0
  9. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/uv.lock +16 -1
  10. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/.github/workflows/publish.yml +0 -0
  11. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/.github/workflows/test.yml +0 -0
  12. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/.gitignore +0 -0
  13. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/.pre-commit-config.yaml +0 -0
  14. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/LICENSE +0 -0
  15. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/justfile +0 -0
  16. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/src/imbi_plugin_github/__init__.py +0 -0
  17. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/src/imbi_plugin_github/_hosts.py +0 -0
  18. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/src/imbi_plugin_github/_repos.py +0 -0
  19. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/tests/__init__.py +0 -0
  20. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/tests/test_deployment.py +0 -0
  21. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/tests/test_hosts.py +0 -0
  22. {imbi_plugin_github-1.3.0 → imbi_plugin_github-1.4.0}/tests/test_identity.py +0 -0
@@ -0,0 +1,128 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What This Is
6
+
7
+ A set of GitHub plugins for the Imbi platform, distributed as a single
8
+ Python package (`imbi_plugin_github`). It ships **three plugin types**
9
+ (identity, deployment, lifecycle), each in **three host flavors**
10
+ (github.com, GitHub Enterprise Cloud, GitHub Enterprise Server) — nine
11
+ plugins total. The Imbi host discovers them through the
12
+ `imbi.plugins` entry points declared in `pyproject.toml`; that table is
13
+ the registration surface — adding a plugin class means adding an entry
14
+ point there.
15
+
16
+ All plugin base classes (`IdentityPlugin`, `DeploymentPlugin`,
17
+ `LifecyclePlugin`, `PluginContext`, `PluginManifest`, the result/dataclass
18
+ types) come from `imbi_common.plugins.base`. That module is the contract
19
+ this package implements against; read it (in the sibling `imbi-common`
20
+ repo) before changing a plugin's method signatures.
21
+
22
+ ## Commands
23
+
24
+ ```bash
25
+ just setup # uv sync --all-groups --all-extras + install pre-commit hooks
26
+ just test # coverage run -m pytest tests + coverage report/xml (fails under 85%)
27
+ just test tests/test_lifecycle.py # one file
28
+ just test tests/test_lifecycle.py::ManifestTestCase # one class/test (passed straight to pytest)
29
+ just lint # pre-commit run --all-files (ruff, ruff-format, tombi, basedpyright)
30
+ just format [FILES] # ruff-format + tombi-format
31
+ ```
32
+
33
+ `just test` with arguments skips the coverage wrapper and runs `pytest`
34
+ directly. `.env`, if present, is passed via `--env-file` (not required
35
+ for the test suite). Type checking is `basedpyright` in **strict** mode
36
+ over `src`. Python is pinned to **3.14**; ruff uses single quotes and a
37
+ 79-char line length.
38
+
39
+ ## Architecture
40
+
41
+ ### The base/subclass/host-flavor pattern
42
+
43
+ Every plugin type follows the same shape: one `_*Base` class holds all
44
+ behavior, and three thin concrete subclasses differ *only* in their
45
+ `_resolve_host(options)` classmethod:
46
+
47
+ - github.com flavor → returns `'github.com'`.
48
+ - GHEC flavor → reads the required `host` option, validates it's a
49
+ `*.ghe.com` tenant via `require_ghec_tenant_host`.
50
+ - GHES flavor → reads the required `host` option, normalized only.
51
+
52
+ `_resolve_host` feeds URL construction, and the three backends route
53
+ differently — this mapping is the single most important thing to keep
54
+ consistent and is **duplicated** across the three modules
55
+ (`identity._endpoints`, `deployment._api_base`, `lifecycle._api_base`):
56
+
57
+ | Host | REST API base | OAuth base (identity only) |
58
+ | ------------------- | ------------------------------ | --------------------------------- |
59
+ | `github.com` | `https://api.github.com` | `https://github.com/login/oauth` |
60
+ | `<tenant>.ghe.com` | `https://api.<tenant>.ghe.com` | `https://<tenant>.ghe.com/login/oauth` |
61
+ | GHES `<host>` | `https://<host>/api/v3` | `https://<host>/login/oauth` |
62
+
63
+ When you change routing for one plugin type, check whether the other two
64
+ need the same change.
65
+
66
+ ### Shared helpers (single sources of truth)
67
+
68
+ - `_hosts.py` — `normalize_host` (strip/validate a bare hostname, reject
69
+ paths/ports/queries) and `require_ghec_tenant_host`. All host-option
70
+ validation lives here.
71
+ - `_repos.py` — `resolve_owner_repo(ctx, host, label)` derives the target
72
+ `(owner, repo)` for deployment and lifecycle calls: it scans
73
+ `ctx.project_links` (preferring the `github-repository` key, skipping
74
+ reserved GitHub path prefixes like `/orgs/`), then falls back to
75
+ `<project_type_slug>/<project_slug>`, and raises `ValueError` with an
76
+ operator-facing message when neither works.
77
+
78
+ ### Plugin invocation contract
79
+
80
+ Plugins are **stateless and single-shot**: the host constructs the
81
+ plugin once and calls methods passing `PluginContext` plus a
82
+ `credentials` dict per call. Identity plugins mint the OAuth access
83
+ token; deployment and lifecycle plugins consume it via
84
+ `credentials['access_token']` (with a `'token'` fallback) and send it as
85
+ a `Bearer` header. A `401` response is converted to
86
+ `PluginAuthenticationFailed` by an httpx response hook so the host's
87
+ retry layer can refresh the actor's identity once before failing the
88
+ user-visible request.
89
+
90
+ ### Behaviors that span multiple files / aren't obvious from one method
91
+
92
+ - **Deployment uses the GitHub Deployments API**, not
93
+ `workflow_dispatch`: Imbi's `Environment` maps 1:1 to GitHub's
94
+ `environment` so protection rules apply server-side. `trigger_deployment`
95
+ sends `auto_merge=False` and `required_contexts=[]` deliberately.
96
+ Promote behavior (semver tag → Deployment vs. raw SHA → tag + Release)
97
+ is decided **host-side** in imbi-api, not here. Per-environment workflow
98
+ inputs ride on the `USES_PLUGIN` graph edge as `env_payloads` and arrive
99
+ as `ctx.environment_config`.
100
+ - **`/check-runs` 403 cache** in `deployment.py`: a process-wide,
101
+ TTL'd cache (keyed by a hash of token+host+repo) suppresses repeated
102
+ 403s when the token lacks scope or Actions is disabled, so opening a
103
+ deploy dialog doesn't fire one wasted 403 per commit.
104
+ - **Lifecycle archive-with-transfer dance** (`lifecycle.py`): when
105
+ `archive_target_org` is set, archive transfers the repo there first.
106
+ GitHub refuses to transfer an *archived* repo, so an already-archived
107
+ source is briefly unarchived → transferred → re-archived. GitHub's
108
+ transfer is **async** (returns 202, repo briefly 404s at the
109
+ destination), so the post-transfer archive PATCH retries on 404 with a
110
+ bounded backoff (`_TRANSFER_ARCHIVE_BACKOFFS`). Unarchive only flips
111
+ `archived` back at the repo's current location — it never transfers
112
+ back, because the original owner isn't tracked.
113
+
114
+ ## Testing
115
+
116
+ Tests mock GitHub's HTTP with **respx** (`asyncio_mode = auto`, so async
117
+ tests need no decorator). Each test builds a `PluginContext` with a
118
+ `github-repository` project link and passes `{'access_token': ...}`
119
+ credentials, mirroring how the host calls in. When adding a GitHub API
120
+ call, add the matching respx route. Coverage must stay ≥ 85%.
121
+
122
+ ## Dependency Management
123
+
124
+ `pyproject.toml` pins the package to PyPI (`pypi.org`) with a 7-day
125
+ `exclude-newer` holdback on third-party packages; the imbi-common and
126
+ `imbi-plugin-*` packages are exempt via `exclude-newer-package` so local
127
+ ecosystem changes aren't held back. After changing dependencies, run
128
+ `uv sync` and verify `uv.lock`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: imbi-plugin-github
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: GitHub identity plugin for Imbi (github.com / GHEC / GHES)
5
5
  Author-email: "Gavin M. Roy" <gavinr@aweber.com>
6
6
  License: BSD-3-Clause
@@ -33,9 +33,9 @@ projects to the right backend.
33
33
 
34
34
  ### Identity
35
35
 
36
- Phase 1 ships the OAuth App flow only; GitHub App installation tokens are
37
- deferred. The access token returned by the OAuth grant is passed straight
38
- to GitHub APIs as a `Bearer` token, so `materialize()` is a no-op.
36
+ Implements the OAuth App flow. The access token returned by the OAuth
37
+ grant is passed straight to GitHub APIs as a `Bearer` token, so
38
+ `materialize()` is a no-op.
39
39
 
40
40
  ### Deployment
41
41
 
@@ -68,7 +68,7 @@ requires admin permission on the target organization.
68
68
  | Option | Required | Description |
69
69
  | ---------------- | --------- | -------------------------------------------------------------------------- |
70
70
  | `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
71
- | `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email`). |
71
+ | `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email repo workflow`). |
72
72
 
73
73
  ## Credentials (identity)
74
74
 
@@ -14,9 +14,9 @@ projects to the right backend.
14
14
 
15
15
  ### Identity
16
16
 
17
- Phase 1 ships the OAuth App flow only; GitHub App installation tokens are
18
- deferred. The access token returned by the OAuth grant is passed straight
19
- to GitHub APIs as a `Bearer` token, so `materialize()` is a no-op.
17
+ Implements the OAuth App flow. The access token returned by the OAuth
18
+ grant is passed straight to GitHub APIs as a `Bearer` token, so
19
+ `materialize()` is a no-op.
20
20
 
21
21
  ### Deployment
22
22
 
@@ -49,7 +49,7 @@ requires admin permission on the target organization.
49
49
  | Option | Required | Description |
50
50
  | ---------------- | --------- | -------------------------------------------------------------------------- |
51
51
  | `host` | GHEC/GHES | Tenant or appliance host (e.g. `tenant.ghe.com`, `github.example.com`). |
52
- | `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email`). |
52
+ | `default_scopes` | no | Space-separated default OAuth scopes (default: `read:user user:email repo workflow`). |
53
53
 
54
54
  ## Credentials (identity)
55
55
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "imbi-plugin-github"
3
- version = "1.3.0"
3
+ version = "1.4.0"
4
4
  description = "GitHub identity plugin for Imbi (github.com / GHEC / GHES)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -121,6 +121,8 @@ max-complexity = 15
121
121
 
122
122
  [tool.uv]
123
123
  default-groups = ["dev"]
124
+ exclude-newer = "7 days"
125
+ exclude-newer-package = { "clickhouse-connect" = false, "imbi-common" = false, "imbi-plugin-aws" = false, "imbi-plugin-github" = false, "imbi-plugin-logzio" = false, "imbi-plugin-oidc" = false, "imbi-plugin-pagerduty" = false, "imbi-plugin-sentry" = false, "imbi-plugin-sonarqube" = false }
124
126
 
125
127
  [[tool.uv.index]]
126
128
  url = "https://pypi.org/simple"
@@ -580,7 +580,7 @@ class _DeploymentBase(DeploymentPlugin):
580
580
  deletions=deletions,
581
581
  )
582
582
 
583
- # -- Tags / Releases (Phase 2 — implemented here for completeness) -----
583
+ # -- Tags / Releases ----------------------------------------------------
584
584
 
585
585
  async def create_tag(
586
586
  self,
@@ -7,9 +7,8 @@ Three concrete subclasses share one base and differ only by host:
7
7
  * :class:`GitHubEnterpriseServerPlugin` — operator-managed GHES; the
8
8
  hostname comes from a manifest option.
9
9
 
10
- Phase 1 implements the OAuth App flow only. GitHub App installation
11
- tokens (org-scoped automation) are deferred service principals
12
- continue to use the legacy ``ServiceApplication`` model.
10
+ The plugins implement the OAuth App flow: the access token from the
11
+ OAuth grant is passed straight to GitHub APIs as a ``Bearer`` token.
13
12
  """
14
13
 
15
14
  from __future__ import annotations
@@ -20,6 +20,16 @@ On project archive the plugin:
20
20
  3. Archives the repo via ``PATCH /repos/{owner}/{repo}`` with
21
21
  ``{"archived": true}``.
22
22
 
23
+ GitHub's repo transfer is asynchronous: ``POST .../transfer`` returns
24
+ ``202 Accepted`` and the repo is briefly unreachable at the
25
+ destination owner. A PATCH fired immediately after the transfer
26
+ therefore 404s, leaving the repo transferred-but-not-archived (see
27
+ the ``archives`` org incidents on the GHEC tenant). The post-transfer
28
+ archive is retried on 404 with a bounded backoff so the common case
29
+ (transfer settles within a few seconds) succeeds, while a genuinely
30
+ stuck transfer still fails fast enough to stay inside the dispatcher's
31
+ per-plugin timeout and surface to the operator.
32
+
23
33
  On unarchive the plugin only flips ``archived`` back to ``false`` at
24
34
  the repo's current location — it does **not** attempt to transfer
25
35
  the repo back to its original org because the original owner is not
@@ -35,6 +45,7 @@ target organization.
35
45
 
36
46
  from __future__ import annotations
37
47
 
48
+ import asyncio
38
49
  import logging
39
50
  import typing
40
51
 
@@ -56,6 +67,13 @@ LOGGER = logging.getLogger(__name__)
56
67
 
57
68
  _HTTP_TIMEOUT_SECONDS = 10.0
58
69
 
70
+ # Backoffs (seconds) between attempts to archive a freshly-transferred
71
+ # repo while GitHub's async transfer settles. len + 1 == total
72
+ # attempts; the sum is kept well under the dispatcher's per-plugin
73
+ # timeout (default 10s) so a stuck transfer fails fast rather than
74
+ # hanging the operator's archive request.
75
+ _TRANSFER_ARCHIVE_BACKOFFS: tuple[float, ...] = (0.5, 1.0, 2.0)
76
+
59
77
 
60
78
  async def _raise_on_401(response: httpx.Response) -> None:
61
79
  """Convert a 401 from GitHub into :class:`PluginAuthenticationFailed`.
@@ -172,8 +190,11 @@ class _LifecycleBase(LifecyclePlugin):
172
190
  repo = str(transferred.get('name') or repo)
173
191
  owner = target_org or current_owner
174
192
  current_owner = owner
175
-
176
- await self._set_archived(client, current_owner, repo, True)
193
+ # The repo may not be reachable at the destination
194
+ # owner yet; tolerate the transfer-settle 404 window.
195
+ await self._archive_after_transfer(client, current_owner, repo)
196
+ else:
197
+ await self._set_archived(client, current_owner, repo, True)
177
198
 
178
199
  return LifecycleResult(
179
200
  status='ok',
@@ -249,6 +270,36 @@ class _LifecycleBase(LifecyclePlugin):
249
270
  )
250
271
  resp.raise_for_status()
251
272
 
273
+ async def _archive_after_transfer(
274
+ self,
275
+ client: httpx.AsyncClient,
276
+ owner: str,
277
+ repo: str,
278
+ ) -> None:
279
+ """Archive a freshly-transferred repo, retrying the 404 window.
280
+
281
+ GitHub's transfer is async: the repo is briefly unreachable at
282
+ the destination owner, so the archive PATCH 404s until the
283
+ transfer settles. Retry only on 404 — any other status (auth,
284
+ permissions, validation) is a real failure and re-raises
285
+ immediately.
286
+ """
287
+ for backoff in (*_TRANSFER_ARCHIVE_BACKOFFS, None):
288
+ try:
289
+ await self._set_archived(client, owner, repo, True)
290
+ return
291
+ except httpx.HTTPStatusError as exc:
292
+ if exc.response.status_code != 404 or backoff is None:
293
+ raise
294
+ LOGGER.info(
295
+ 'Repo %s/%s not yet reachable after transfer; '
296
+ 'retrying archive in %ss',
297
+ owner,
298
+ repo,
299
+ backoff,
300
+ )
301
+ await asyncio.sleep(backoff)
302
+
252
303
  async def _transfer(
253
304
  self,
254
305
  client: httpx.AsyncClient,
@@ -8,6 +8,7 @@ from absent links, and the 401 -> PluginAuthenticationFailed path.
8
8
  """
9
9
 
10
10
  import unittest
11
+ import unittest.mock
11
12
 
12
13
  import httpx
13
14
  import respx
@@ -179,6 +180,127 @@ class ArchiveTestCase(unittest.IsolatedAsyncioTestCase):
179
180
  'https://github.com/octo-archive/demo',
180
181
  )
181
182
 
183
+ @respx.mock
184
+ async def test_archive_transfer_retries_settle_404(self) -> None:
185
+ # GitHub's transfer is async (202); the repo is briefly
186
+ # unreachable at the destination, so the first archive PATCH
187
+ # 404s. The plugin must retry and succeed once it settles.
188
+ respx.get('https://api.github.com/repos/octo/demo').mock(
189
+ return_value=httpx.Response(
190
+ 200,
191
+ json={
192
+ 'archived': False,
193
+ 'owner': {'login': 'octo'},
194
+ 'name': 'demo',
195
+ },
196
+ )
197
+ )
198
+ respx.post('https://api.github.com/repos/octo/demo/transfer').mock(
199
+ return_value=httpx.Response(
200
+ 202,
201
+ json={'name': 'demo', 'owner': {'login': 'octo-archive'}},
202
+ )
203
+ )
204
+ archive_route = respx.patch(
205
+ 'https://api.github.com/repos/octo-archive/demo'
206
+ ).mock(
207
+ side_effect=[
208
+ httpx.Response(404, json={'message': 'Not Found'}),
209
+ httpx.Response(404, json={'message': 'Not Found'}),
210
+ httpx.Response(200, json={}),
211
+ ]
212
+ )
213
+
214
+ plugin = GitHubLifecyclePlugin()
215
+ with unittest.mock.patch(
216
+ 'imbi_plugin_github.lifecycle.asyncio.sleep'
217
+ ) as sleep:
218
+ result = await plugin.on_project_archived(
219
+ _ctx(options={'archive_target_org': 'octo-archive'}),
220
+ _CREDS,
221
+ )
222
+
223
+ self.assertEqual(result.status, 'ok')
224
+ self.assertEqual(archive_route.calls.call_count, 3)
225
+ # Two failed attempts → two backoff sleeps before success.
226
+ self.assertEqual(sleep.await_count, 2)
227
+
228
+ @respx.mock
229
+ async def test_archive_transfer_exhausts_retries_on_persistent_404(
230
+ self,
231
+ ) -> None:
232
+ # If the transfer never settles within the retry budget the
233
+ # final 404 must propagate so the dispatcher records a failure.
234
+ respx.get('https://api.github.com/repos/octo/demo').mock(
235
+ return_value=httpx.Response(
236
+ 200,
237
+ json={
238
+ 'archived': False,
239
+ 'owner': {'login': 'octo'},
240
+ 'name': 'demo',
241
+ },
242
+ )
243
+ )
244
+ respx.post('https://api.github.com/repos/octo/demo/transfer').mock(
245
+ return_value=httpx.Response(
246
+ 202,
247
+ json={'name': 'demo', 'owner': {'login': 'octo-archive'}},
248
+ )
249
+ )
250
+ archive_route = respx.patch(
251
+ 'https://api.github.com/repos/octo-archive/demo'
252
+ ).mock(return_value=httpx.Response(404, json={'message': 'Not Found'}))
253
+
254
+ plugin = GitHubLifecyclePlugin()
255
+ with unittest.mock.patch(
256
+ 'imbi_plugin_github.lifecycle.asyncio.sleep'
257
+ ) as sleep:
258
+ with self.assertRaises(httpx.HTTPStatusError):
259
+ await plugin.on_project_archived(
260
+ _ctx(options={'archive_target_org': 'octo-archive'}),
261
+ _CREDS,
262
+ )
263
+
264
+ # Three backoffs configured → four total attempts.
265
+ self.assertEqual(archive_route.calls.call_count, 4)
266
+ self.assertEqual(sleep.await_count, 3)
267
+
268
+ @respx.mock
269
+ async def test_archive_transfer_does_not_retry_non_404(self) -> None:
270
+ # A non-404 (e.g. permissions) is a real failure: raise at once.
271
+ respx.get('https://api.github.com/repos/octo/demo').mock(
272
+ return_value=httpx.Response(
273
+ 200,
274
+ json={
275
+ 'archived': False,
276
+ 'owner': {'login': 'octo'},
277
+ 'name': 'demo',
278
+ },
279
+ )
280
+ )
281
+ respx.post('https://api.github.com/repos/octo/demo/transfer').mock(
282
+ return_value=httpx.Response(
283
+ 202,
284
+ json={'name': 'demo', 'owner': {'login': 'octo-archive'}},
285
+ )
286
+ )
287
+ archive_route = respx.patch(
288
+ 'https://api.github.com/repos/octo-archive/demo'
289
+ ).mock(return_value=httpx.Response(403, json={'message': 'Forbidden'}))
290
+
291
+ plugin = GitHubLifecyclePlugin()
292
+ with unittest.mock.patch(
293
+ 'imbi_plugin_github.lifecycle.asyncio.sleep'
294
+ ) as sleep:
295
+ with self.assertRaises(httpx.HTTPStatusError):
296
+ await plugin.on_project_archived(
297
+ _ctx(options={'archive_target_org': 'octo-archive'}),
298
+ _CREDS,
299
+ )
300
+
301
+ self.assertEqual(archive_route.calls.call_count, 1)
302
+ self.assertEqual(sleep.await_count, 0)
303
+
182
304
  @respx.mock
183
305
  async def test_archive_transfer_when_already_archived(self) -> None:
184
306
  # GitHub forbids transferring archived repos, so the plugin must
@@ -2,6 +2,21 @@ version = 1
2
2
  revision = 3
3
3
  requires-python = ">=3.14"
4
4
 
5
+ [options]
6
+ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
7
+ exclude-newer-span = "P7D"
8
+
9
+ [options.exclude-newer-package]
10
+ imbi-common = false
11
+ imbi-plugin-pagerduty = false
12
+ imbi-plugin-github = false
13
+ imbi-plugin-logzio = false
14
+ imbi-plugin-aws = false
15
+ imbi-plugin-sonarqube = false
16
+ clickhouse-connect = false
17
+ imbi-plugin-sentry = false
18
+ imbi-plugin-oidc = false
19
+
5
20
  [[package]]
6
21
  name = "aiohappyeyeballs"
7
22
  version = "2.6.1"
@@ -688,7 +703,7 @@ server = [
688
703
 
689
704
  [[package]]
690
705
  name = "imbi-plugin-github"
691
- version = "1.2.0"
706
+ version = "1.4.0"
692
707
  source = { editable = "." }
693
708
  dependencies = [
694
709
  { name = "httpx" },