imbi-plugin-github 1.1.0__tar.gz → 1.3.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.
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/PKG-INFO +6 -4
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/README.md +4 -2
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/pyproject.toml +8 -8
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/src/imbi_plugin_github/deployment.py +220 -37
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/tests/test_deployment.py +277 -50
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/uv.lock +36 -26
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/.github/workflows/publish.yml +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/.github/workflows/test.yml +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/.gitignore +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/.pre-commit-config.yaml +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/LICENSE +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/justfile +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/src/imbi_plugin_github/__init__.py +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/src/imbi_plugin_github/_hosts.py +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/src/imbi_plugin_github/_repos.py +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/src/imbi_plugin_github/identity.py +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/src/imbi_plugin_github/lifecycle.py +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/tests/__init__.py +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/tests/test_hosts.py +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/tests/test_identity.py +0 -0
- {imbi_plugin_github-1.1.0 → imbi_plugin_github-1.3.0}/tests/test_lifecycle.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: imbi-plugin-github
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.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
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.14
|
|
14
14
|
Requires-Python: >=3.14
|
|
15
15
|
Requires-Dist: httpx>=0.27
|
|
16
|
-
Requires-Dist: imbi-common>=
|
|
16
|
+
Requires-Dist: imbi-common>=2.5.5
|
|
17
17
|
Requires-Dist: pydantic>=2
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -40,8 +40,10 @@ to GitHub APIs as a `Bearer` token, so `materialize()` is a no-op.
|
|
|
40
40
|
### Deployment
|
|
41
41
|
|
|
42
42
|
Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
|
|
43
|
-
plus tag and release creation.
|
|
44
|
-
|
|
43
|
+
plus tag and release creation. Promote behaviour is inferred from the
|
|
44
|
+
ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
|
|
45
|
+
+ Release). Per-env workflow inputs ride on the `USES_PLUGIN` edge as
|
|
46
|
+
`env_payloads` and arrive on `PluginContext.environment_config`.
|
|
45
47
|
|
|
46
48
|
### Lifecycle
|
|
47
49
|
|
|
@@ -21,8 +21,10 @@ to GitHub APIs as a `Bearer` token, so `materialize()` is a no-op.
|
|
|
21
21
|
### Deployment
|
|
22
22
|
|
|
23
23
|
Drives the GitHub Deployments API (`POST /repos/{owner}/{repo}/deployments`)
|
|
24
|
-
plus tag and release creation.
|
|
25
|
-
|
|
24
|
+
plus tag and release creation. Promote behaviour is inferred from the
|
|
25
|
+
ref shape by the host (semver → trigger Deployment, raw SHA → cut tag
|
|
26
|
+
+ Release). Per-env workflow inputs ride on the `USES_PLUGIN` edge as
|
|
27
|
+
`env_payloads` and arrive on `PluginContext.environment_config`.
|
|
26
28
|
|
|
27
29
|
### Lifecycle
|
|
28
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "imbi-plugin-github"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.3.0"
|
|
4
4
|
description = "GitHub identity plugin for Imbi (github.com / GHEC / GHES)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.14"
|
|
@@ -16,7 +16,7 @@ classifiers = [
|
|
|
16
16
|
]
|
|
17
17
|
dependencies = [
|
|
18
18
|
"httpx>=0.27",
|
|
19
|
-
"imbi-common>=
|
|
19
|
+
"imbi-common>=2.5.5",
|
|
20
20
|
"pydantic>=2",
|
|
21
21
|
]
|
|
22
22
|
|
|
@@ -48,9 +48,6 @@ dist = ["build", "twine", "wheel"]
|
|
|
48
48
|
requires = ["hatchling"]
|
|
49
49
|
build-backend = "hatchling.build"
|
|
50
50
|
|
|
51
|
-
[tool.hatch.build.targets.wheel]
|
|
52
|
-
packages = ["src/imbi_plugin_github"]
|
|
53
|
-
|
|
54
51
|
[tool.coverage.report]
|
|
55
52
|
exclude_also = ["typing.TYPE_CHECKING"]
|
|
56
53
|
fail_under = 85
|
|
@@ -60,9 +57,8 @@ show_missing = true
|
|
|
60
57
|
branch = true
|
|
61
58
|
source = ["src/imbi_plugin_github"]
|
|
62
59
|
|
|
63
|
-
[tool.
|
|
64
|
-
|
|
65
|
-
asyncio_default_fixture_loop_scope = "function"
|
|
60
|
+
[tool.hatch.build.targets.wheel]
|
|
61
|
+
packages = ["src/imbi_plugin_github"]
|
|
66
62
|
|
|
67
63
|
[tool.pyright]
|
|
68
64
|
executionEnvironments = [
|
|
@@ -74,6 +70,10 @@ reportMissingSuperCall = "hint"
|
|
|
74
70
|
reportMissingTypeStubs = false
|
|
75
71
|
typeCheckingMode = "strict"
|
|
76
72
|
|
|
73
|
+
[tool.pytest.ini_options]
|
|
74
|
+
asyncio_mode = "auto"
|
|
75
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
76
|
+
|
|
77
77
|
[tool.ruff]
|
|
78
78
|
line-length = 79
|
|
79
79
|
target-version = "py314"
|
|
@@ -38,8 +38,10 @@ from imbi_common.plugins.base import (
|
|
|
38
38
|
Commit,
|
|
39
39
|
CompareResult,
|
|
40
40
|
CredentialField,
|
|
41
|
+
DeploymentEventStatus,
|
|
41
42
|
DeploymentPlugin,
|
|
42
43
|
DeploymentRun,
|
|
44
|
+
OpsLogTemplate,
|
|
43
45
|
PluginContext,
|
|
44
46
|
PluginEdgeLabel,
|
|
45
47
|
PluginManifest,
|
|
@@ -47,6 +49,7 @@ from imbi_common.plugins.base import (
|
|
|
47
49
|
Ref,
|
|
48
50
|
RefInfo,
|
|
49
51
|
ReleaseInfo,
|
|
52
|
+
RemoteDeployment,
|
|
50
53
|
WorkflowFile,
|
|
51
54
|
)
|
|
52
55
|
from imbi_common.plugins.errors import PluginAuthenticationFailed
|
|
@@ -669,8 +672,9 @@ class _DeploymentBase(DeploymentPlugin):
|
|
|
669
672
|
promote refs are often freshly-cut tags whose CI hasn't run yet,
|
|
670
673
|
and the deploy workflow itself is what we're waiting on.
|
|
671
674
|
|
|
672
|
-
Payload precedence (highest
|
|
673
|
-
``
|
|
675
|
+
Payload precedence (lowest → highest): plugin assignment
|
|
676
|
+
``env_payloads[env_slug]`` (carried by the host on
|
|
677
|
+
``ctx.environment_config``) below the ``inputs`` map from the
|
|
674
678
|
caller. The ``ref`` and ``environment`` are not part of the
|
|
675
679
|
payload — they're top-level fields on the deployment object.
|
|
676
680
|
"""
|
|
@@ -678,12 +682,9 @@ class _DeploymentBase(DeploymentPlugin):
|
|
|
678
682
|
raise ValueError(
|
|
679
683
|
'trigger_deployment requires PluginContext.environment'
|
|
680
684
|
)
|
|
681
|
-
merged_payload: dict[str, typing.Any] = dict(
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
merged_payload.update(
|
|
685
|
-
typing.cast(dict[str, typing.Any], env_payload)
|
|
686
|
-
)
|
|
685
|
+
merged_payload: dict[str, typing.Any] = dict(ctx.environment_config)
|
|
686
|
+
if inputs:
|
|
687
|
+
merged_payload.update(inputs)
|
|
687
688
|
async with self._client(ctx, credentials) as client:
|
|
688
689
|
resp = await client.post(
|
|
689
690
|
'/deployments',
|
|
@@ -717,7 +718,7 @@ class _DeploymentBase(DeploymentPlugin):
|
|
|
717
718
|
"""List ``.github/workflows/*.yml`` registered for the repo.
|
|
718
719
|
|
|
719
720
|
Used by the UI to populate a workflow dropdown when an operator
|
|
720
|
-
|
|
721
|
+
configures plugin assignment ``env_payloads``. Returns only
|
|
721
722
|
``active`` workflows by default; callers that need disabled
|
|
722
723
|
entries can filter the result themselves. GitHub caps the
|
|
723
724
|
``/actions/workflows`` page at 100 — that's more than enough for
|
|
@@ -839,37 +840,186 @@ class _DeploymentBase(DeploymentPlugin):
|
|
|
839
840
|
else None,
|
|
840
841
|
)
|
|
841
842
|
|
|
843
|
+
async def list_recent_deployments(
|
|
844
|
+
self,
|
|
845
|
+
ctx: PluginContext,
|
|
846
|
+
credentials: dict[str, str],
|
|
847
|
+
environments: list[str],
|
|
848
|
+
limit: int = 1,
|
|
849
|
+
) -> list[RemoteDeployment]:
|
|
850
|
+
"""Return the latest ``limit`` deployments per environment.
|
|
851
|
+
|
|
852
|
+
Fans out one ``GET /deployments?environment={env}`` call per
|
|
853
|
+
requested environment in parallel, then for each returned
|
|
854
|
+
deployment fetches the latest status via ``GET
|
|
855
|
+
/deployments/{id}/statuses?per_page=1``. Environments the
|
|
856
|
+
remote does not recognise are silently skipped so a partial
|
|
857
|
+
resync still returns the deployments that do exist (an env
|
|
858
|
+
the repo simply hasn't deployed to yet is the common case).
|
|
859
|
+
|
|
860
|
+
The host calls this from the resync flow only when webhook
|
|
861
|
+
delivery has lapsed, so we keep the fan-out modest (``limit=1``
|
|
862
|
+
is the host's default) and let the host walk further history
|
|
863
|
+
with explicit pagination if it ever needs to.
|
|
864
|
+
"""
|
|
865
|
+
page_size = max(1, min(limit, 100))
|
|
866
|
+
async with self._client(ctx, credentials) as client:
|
|
867
|
+
per_env = await asyncio.gather(
|
|
868
|
+
*(
|
|
869
|
+
self._list_deployments_for_env(client, env, page_size)
|
|
870
|
+
for env in environments
|
|
871
|
+
)
|
|
872
|
+
)
|
|
873
|
+
return [observed for group in per_env for observed in group]
|
|
874
|
+
|
|
875
|
+
async def _list_deployments_for_env(
|
|
876
|
+
self,
|
|
877
|
+
client: httpx.AsyncClient,
|
|
878
|
+
environment: str,
|
|
879
|
+
page_size: int,
|
|
880
|
+
) -> list[RemoteDeployment]:
|
|
881
|
+
try:
|
|
882
|
+
resp = await client.get(
|
|
883
|
+
'/deployments',
|
|
884
|
+
params={
|
|
885
|
+
'environment': environment,
|
|
886
|
+
'per_page': str(page_size),
|
|
887
|
+
},
|
|
888
|
+
)
|
|
889
|
+
if resp.status_code == 404:
|
|
890
|
+
# Repo or environment unknown on the remote — treat as
|
|
891
|
+
# "nothing to backfill" rather than failing the resync.
|
|
892
|
+
return []
|
|
893
|
+
resp.raise_for_status()
|
|
894
|
+
except httpx.HTTPError:
|
|
895
|
+
LOGGER.warning(
|
|
896
|
+
'Failed to list deployments for env=%s',
|
|
897
|
+
environment,
|
|
898
|
+
exc_info=True,
|
|
899
|
+
)
|
|
900
|
+
return []
|
|
901
|
+
try:
|
|
902
|
+
deployments = typing.cast(list[dict[str, typing.Any]], resp.json())
|
|
903
|
+
except ValueError:
|
|
904
|
+
LOGGER.warning(
|
|
905
|
+
'Failed to parse deployments payload for env=%s',
|
|
906
|
+
environment,
|
|
907
|
+
)
|
|
908
|
+
return []
|
|
909
|
+
observed: list[RemoteDeployment] = []
|
|
910
|
+
for deployment in deployments:
|
|
911
|
+
run = await self._observe_deployment(
|
|
912
|
+
client, environment, deployment
|
|
913
|
+
)
|
|
914
|
+
if run is not None:
|
|
915
|
+
observed.append(run)
|
|
916
|
+
return observed
|
|
917
|
+
|
|
918
|
+
async def _observe_deployment(
|
|
919
|
+
self,
|
|
920
|
+
client: httpx.AsyncClient,
|
|
921
|
+
environment: str,
|
|
922
|
+
deployment: dict[str, typing.Any],
|
|
923
|
+
) -> RemoteDeployment | None:
|
|
924
|
+
deployment_id = deployment.get('id')
|
|
925
|
+
sha = deployment.get('sha')
|
|
926
|
+
if not deployment_id or not sha:
|
|
927
|
+
# GitHub always returns both, but defend the resync path
|
|
928
|
+
# against a malformed response — we'd rather skip one row
|
|
929
|
+
# than corrupt the graph by inventing identifiers.
|
|
930
|
+
return None
|
|
931
|
+
created_at = _parse_iso(deployment.get('created_at')) or (
|
|
932
|
+
datetime.datetime.now(datetime.UTC)
|
|
933
|
+
)
|
|
934
|
+
status, status_url = await self._latest_status(
|
|
935
|
+
client, str(deployment_id)
|
|
936
|
+
)
|
|
937
|
+
ref_value = deployment.get('ref')
|
|
938
|
+
description = deployment.get('description')
|
|
939
|
+
deployment_url = deployment.get('url') or deployment.get('html_url')
|
|
940
|
+
creator_login: str | None = None
|
|
941
|
+
creator_raw = deployment.get('creator')
|
|
942
|
+
if isinstance(creator_raw, dict):
|
|
943
|
+
creator_dict = typing.cast(dict[str, typing.Any], creator_raw)
|
|
944
|
+
login = creator_dict.get('login')
|
|
945
|
+
if isinstance(login, str) and login:
|
|
946
|
+
creator_login = login
|
|
947
|
+
return RemoteDeployment(
|
|
948
|
+
environment=environment,
|
|
949
|
+
sha=str(sha),
|
|
950
|
+
ref=str(ref_value) if ref_value else None,
|
|
951
|
+
status=status,
|
|
952
|
+
created_at=created_at,
|
|
953
|
+
external_run_id=str(deployment_id),
|
|
954
|
+
run_url=status_url,
|
|
955
|
+
deployment_url=str(deployment_url) if deployment_url else None,
|
|
956
|
+
description=str(description) if description else None,
|
|
957
|
+
creator=creator_login,
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
async def _latest_status(
|
|
961
|
+
self, client: httpx.AsyncClient, deployment_id: str
|
|
962
|
+
) -> tuple[DeploymentEventStatus, str | None]:
|
|
963
|
+
"""Return the canonical event status + workflow log URL.
|
|
964
|
+
|
|
965
|
+
Falls back to ``'pending'`` whenever the deploy workflow has
|
|
966
|
+
not yet posted a status: a freshly-created deployment with no
|
|
967
|
+
statuses is structurally identical to one whose workflow has
|
|
968
|
+
not started, and ``pending`` is the host's vocabulary for
|
|
969
|
+
both. Network / parse errors degrade the same way so resync
|
|
970
|
+
is never blocked by a single noisy row.
|
|
971
|
+
"""
|
|
972
|
+
try:
|
|
973
|
+
resp = await client.get(
|
|
974
|
+
f'/deployments/{deployment_id}/statuses',
|
|
975
|
+
params={'per_page': '1'},
|
|
976
|
+
)
|
|
977
|
+
except httpx.HTTPError:
|
|
978
|
+
return 'pending', None
|
|
979
|
+
if resp.status_code != 200:
|
|
980
|
+
return 'pending', None
|
|
981
|
+
try:
|
|
982
|
+
statuses = typing.cast(list[dict[str, typing.Any]], resp.json())
|
|
983
|
+
except ValueError:
|
|
984
|
+
return 'pending', None
|
|
985
|
+
if not statuses:
|
|
986
|
+
return 'pending', None
|
|
987
|
+
latest = statuses[0]
|
|
988
|
+
state = str(latest.get('state') or '').lower()
|
|
989
|
+
log_url = latest.get('log_url') or latest.get('target_url')
|
|
990
|
+
return _to_event_status(state), str(log_url) if log_url else None
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
def _to_event_status(github_state: str) -> DeploymentEventStatus:
|
|
994
|
+
"""Map a GitHub deployment-status ``state`` to the host vocabulary.
|
|
995
|
+
|
|
996
|
+
Unknown states fold to ``pending`` rather than raising so a single
|
|
997
|
+
novel value on the remote does not break resync for the whole
|
|
998
|
+
project. ``inactive`` on GitHub means a newer deployment for the
|
|
999
|
+
same environment superseded this one, which the host models as
|
|
1000
|
+
``rolled_back`` on the ``DeploymentEvent``.
|
|
1001
|
+
"""
|
|
1002
|
+
if github_state in {'pending', 'queued', 'waiting'}:
|
|
1003
|
+
return 'pending'
|
|
1004
|
+
if github_state == 'in_progress':
|
|
1005
|
+
return 'in_progress'
|
|
1006
|
+
if github_state == 'success':
|
|
1007
|
+
return 'success'
|
|
1008
|
+
if github_state in {'failure', 'error'}:
|
|
1009
|
+
return 'failed'
|
|
1010
|
+
if github_state == 'inactive':
|
|
1011
|
+
return 'rolled_back'
|
|
1012
|
+
return 'pending'
|
|
1013
|
+
|
|
842
1014
|
|
|
843
1015
|
_COMMON_OPTIONS: list[PluginOption] = []
|
|
844
1016
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
from_labels=['ProjectType', 'Project'],
|
|
852
|
-
to_labels=['Environment'],
|
|
853
|
-
properties={
|
|
854
|
-
# 'release' -> create_tag + create_release; no deployment
|
|
855
|
-
# (the repo's ``on: release: [published]``
|
|
856
|
-
# trigger handles it).
|
|
857
|
-
# 'deployment' -> POST /deployments only (no tag/release).
|
|
858
|
-
'action': 'str',
|
|
859
|
-
# Free-form key/value pairs forwarded as the GitHub
|
|
860
|
-
# deployment ``payload``. The deploy workflow reads them
|
|
861
|
-
# via ``github.event.deployment.payload`` -- so this is
|
|
862
|
-
# where operators stash things like a target cluster name,
|
|
863
|
-
# a feature flag bundle, etc. Empty by default.
|
|
864
|
-
'payload': 'dict[str, str]',
|
|
865
|
-
# Per-environment identity override. Lets ``production``
|
|
866
|
-
# post the deployment as a different OAuth identity than
|
|
867
|
-
# ``staging`` -- handy when prod requires a service
|
|
868
|
-
# account with extra deploy scopes.
|
|
869
|
-
'identity_plugin_id': 'str',
|
|
870
|
-
},
|
|
871
|
-
),
|
|
872
|
-
]
|
|
1017
|
+
# Promote behaviour is now inferred from the ``body.tag`` shape on the
|
|
1018
|
+
# imbi-api side: semver tags trigger a Deployment, raw SHAs cut a tag
|
|
1019
|
+
# + GitHub Release. Per-env workflow input overrides live on the
|
|
1020
|
+
# ``USES_PLUGIN`` edge under ``env_payloads`` (keyed by env slug),
|
|
1021
|
+
# resolved by the host and passed in via ``ctx.environment_config``.
|
|
1022
|
+
_COMMON_EDGE_LABELS: list[PluginEdgeLabel] = []
|
|
873
1023
|
|
|
874
1024
|
_COMMON_CREDENTIALS: list[CredentialField] = [
|
|
875
1025
|
CredentialField(
|
|
@@ -884,6 +1034,33 @@ _COMMON_CREDENTIALS: list[CredentialField] = [
|
|
|
884
1034
|
]
|
|
885
1035
|
|
|
886
1036
|
|
|
1037
|
+
# Templates for the operations-log JSON payload the API writes from
|
|
1038
|
+
# ``_record_deployment_event`` in imbi-api: ``{action, plugin_slug,
|
|
1039
|
+
# run_url, release_url, from_environment}``. Row-level fields
|
|
1040
|
+
# ``version`` and ``environment`` (from the entry's
|
|
1041
|
+
# ``environment_slug``/``environment.name``) are also in scope.
|
|
1042
|
+
_COMMON_OPS_LOG_TEMPLATES: dict[str, OpsLogTemplate] = {
|
|
1043
|
+
'deploy': OpsLogTemplate(
|
|
1044
|
+
label='Deployed {{version}} to {{environment}}',
|
|
1045
|
+
summary='deployed',
|
|
1046
|
+
),
|
|
1047
|
+
'redeploy': OpsLogTemplate(
|
|
1048
|
+
label='Re-deployed {{version}} to {{environment}}',
|
|
1049
|
+
summary='re-deployed',
|
|
1050
|
+
),
|
|
1051
|
+
'promote': OpsLogTemplate(
|
|
1052
|
+
label=(
|
|
1053
|
+
'Promoted {{from_environment}} to {{environment}} as {{version}}.'
|
|
1054
|
+
),
|
|
1055
|
+
summary='promoted',
|
|
1056
|
+
),
|
|
1057
|
+
'resync': OpsLogTemplate(
|
|
1058
|
+
label='Recorded {{version}} deploy in {{environment}}',
|
|
1059
|
+
summary='recorded a deploy in',
|
|
1060
|
+
),
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
|
|
887
1064
|
class GitHubDeploymentPlugin(_DeploymentBase):
|
|
888
1065
|
manifest = PluginManifest(
|
|
889
1066
|
slug='github-deployment',
|
|
@@ -896,9 +1073,11 @@ class GitHubDeploymentPlugin(_DeploymentBase):
|
|
|
896
1073
|
'server-side.'
|
|
897
1074
|
),
|
|
898
1075
|
plugin_type='deployment',
|
|
1076
|
+
supports_deployment_sync=True,
|
|
899
1077
|
options=_COMMON_OPTIONS,
|
|
900
1078
|
credentials=_COMMON_CREDENTIALS,
|
|
901
1079
|
edge_labels=_COMMON_EDGE_LABELS,
|
|
1080
|
+
ops_log_templates=_COMMON_OPS_LOG_TEMPLATES,
|
|
902
1081
|
)
|
|
903
1082
|
|
|
904
1083
|
@classmethod
|
|
@@ -916,6 +1095,7 @@ class GitHubEnterpriseCloudDeploymentPlugin(_DeploymentBase):
|
|
|
916
1095
|
'``on: deployment_status``) in their deploy workflow.'
|
|
917
1096
|
),
|
|
918
1097
|
plugin_type='deployment',
|
|
1098
|
+
supports_deployment_sync=True,
|
|
919
1099
|
options=[
|
|
920
1100
|
PluginOption(
|
|
921
1101
|
name='host',
|
|
@@ -928,6 +1108,7 @@ class GitHubEnterpriseCloudDeploymentPlugin(_DeploymentBase):
|
|
|
928
1108
|
],
|
|
929
1109
|
credentials=_COMMON_CREDENTIALS,
|
|
930
1110
|
edge_labels=_COMMON_EDGE_LABELS,
|
|
1111
|
+
ops_log_templates=_COMMON_OPS_LOG_TEMPLATES,
|
|
931
1112
|
)
|
|
932
1113
|
|
|
933
1114
|
@classmethod
|
|
@@ -948,6 +1129,7 @@ class GitHubEnterpriseServerDeploymentPlugin(_DeploymentBase):
|
|
|
948
1129
|
'in their deploy workflow.'
|
|
949
1130
|
),
|
|
950
1131
|
plugin_type='deployment',
|
|
1132
|
+
supports_deployment_sync=True,
|
|
951
1133
|
options=[
|
|
952
1134
|
PluginOption(
|
|
953
1135
|
name='host',
|
|
@@ -959,6 +1141,7 @@ class GitHubEnterpriseServerDeploymentPlugin(_DeploymentBase):
|
|
|
959
1141
|
],
|
|
960
1142
|
credentials=_COMMON_CREDENTIALS,
|
|
961
1143
|
edge_labels=_COMMON_EDGE_LABELS,
|
|
1144
|
+
ops_log_templates=_COMMON_OPS_LOG_TEMPLATES,
|
|
962
1145
|
)
|
|
963
1146
|
|
|
964
1147
|
@classmethod
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Smoke tests for the GitHub deployment plugins."""
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
3
4
|
import json
|
|
4
5
|
import time
|
|
5
6
|
import unittest
|
|
@@ -62,36 +63,30 @@ class ManifestTestCase(unittest.TestCase):
|
|
|
62
63
|
self.assertIsInstance(cls(), DeploymentPlugin)
|
|
63
64
|
self.assertEqual(cls.manifest.plugin_type, 'deployment')
|
|
64
65
|
|
|
65
|
-
def
|
|
66
|
-
# Every concrete subclass needs the DEPLOYS_VIA declaration so
|
|
67
|
-
# admins can wire up per-env config from the plugin-edge UI for
|
|
68
|
-
# any GitHub flavor (.com / GHEC / GHES).
|
|
66
|
+
def test_all_advertise_supports_deployment_sync(self) -> None:
|
|
69
67
|
for cls in (
|
|
70
68
|
GitHubDeploymentPlugin,
|
|
71
69
|
GitHubEnterpriseCloudDeploymentPlugin,
|
|
72
70
|
GitHubEnterpriseServerDeploymentPlugin,
|
|
73
71
|
):
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
for e in cls.manifest.edge_labels
|
|
78
|
-
if e.name == 'DEPLOYS_VIA'
|
|
79
|
-
),
|
|
80
|
-
None,
|
|
72
|
+
self.assertTrue(
|
|
73
|
+
cls.manifest.supports_deployment_sync,
|
|
74
|
+
f'{cls.__name__} must opt in to deployment sync',
|
|
81
75
|
)
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
|
|
77
|
+
def test_no_legacy_deploys_via_edge_declared(self) -> None:
|
|
78
|
+
# Promote behaviour is inferred from the ref shape and per-env
|
|
79
|
+
# payloads ride on the USES_PLUGIN edge (``env_payloads``). No
|
|
80
|
+
# plugin should declare a leftover ``DEPLOYS_VIA`` edge.
|
|
81
|
+
for cls in (
|
|
82
|
+
GitHubDeploymentPlugin,
|
|
83
|
+
GitHubEnterpriseCloudDeploymentPlugin,
|
|
84
|
+
GitHubEnterpriseServerDeploymentPlugin,
|
|
85
|
+
):
|
|
86
|
+
self.assertFalse(
|
|
87
|
+
any(e.name == 'DEPLOYS_VIA' for e in cls.manifest.edge_labels),
|
|
88
|
+
f'{cls.__name__} still declares DEPLOYS_VIA',
|
|
84
89
|
)
|
|
85
|
-
assert edge is not None
|
|
86
|
-
self.assertEqual(set(edge.from_labels), {'ProjectType', 'Project'})
|
|
87
|
-
self.assertEqual(edge.to_labels, ['Environment'])
|
|
88
|
-
self.assertIn('action', edge.properties)
|
|
89
|
-
self.assertEqual(edge.properties['payload'], 'dict[str, str]')
|
|
90
|
-
self.assertIn('identity_plugin_id', edge.properties)
|
|
91
|
-
# Properties from the prior workflow_dispatch design must
|
|
92
|
-
# not be carried forward.
|
|
93
|
-
self.assertNotIn('workflow', edge.properties)
|
|
94
|
-
self.assertNotIn('inputs', edge.properties)
|
|
95
90
|
|
|
96
91
|
def test_owner_repo_required(self) -> None:
|
|
97
92
|
plugin = GitHubDeploymentPlugin()
|
|
@@ -648,9 +643,10 @@ class TriggerDeploymentTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
648
643
|
|
|
649
644
|
@respx.mock
|
|
650
645
|
async def test_trigger_uses_environment_config_payload(self) -> None:
|
|
651
|
-
#
|
|
652
|
-
# ``
|
|
653
|
-
#
|
|
646
|
+
# ``ctx.environment_config`` carries the per-env payload dict
|
|
647
|
+
# (``env_payloads[env_slug]`` from the USES_PLUGIN edge,
|
|
648
|
+
# resolved by the host). Caller-supplied ``inputs`` layer on
|
|
649
|
+
# top, so a manual override wins on shared keys.
|
|
654
650
|
deploy = respx.post(
|
|
655
651
|
'https://api.github.com/repos/octo/demo/deployments'
|
|
656
652
|
).mock(return_value=httpx.Response(201, json={'id': 1, 'url': ''}))
|
|
@@ -658,43 +654,23 @@ class TriggerDeploymentTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
658
654
|
ctx = _ctx(
|
|
659
655
|
environment='production',
|
|
660
656
|
environment_config={
|
|
661
|
-
'
|
|
662
|
-
|
|
663
|
-
'feature_flag': 'on',
|
|
664
|
-
}
|
|
657
|
+
'cluster': 'prod-east',
|
|
658
|
+
'feature_flag': 'on',
|
|
665
659
|
},
|
|
666
660
|
)
|
|
667
661
|
await plugin.trigger_deployment(
|
|
668
662
|
ctx,
|
|
669
663
|
_CREDS,
|
|
670
664
|
ref_or_sha='v1.2.3',
|
|
671
|
-
inputs={'cluster': '
|
|
665
|
+
inputs={'cluster': 'override', 'extra': 'kept'},
|
|
672
666
|
)
|
|
673
667
|
body = json.loads(deploy.calls.last.request.read())
|
|
674
668
|
self.assertEqual(body['ref'], 'v1.2.3')
|
|
675
669
|
self.assertEqual(body['environment'], 'production')
|
|
676
670
|
self.assertEqual(
|
|
677
671
|
body['payload'],
|
|
678
|
-
{'cluster': '
|
|
679
|
-
)
|
|
680
|
-
|
|
681
|
-
@respx.mock
|
|
682
|
-
async def test_trigger_ignores_non_dict_env_payload(self) -> None:
|
|
683
|
-
# A malformed edge property (string instead of dict) should not
|
|
684
|
-
# crash the plugin — we drop it and fall back to caller inputs.
|
|
685
|
-
deploy = respx.post(
|
|
686
|
-
'https://api.github.com/repos/octo/demo/deployments'
|
|
687
|
-
).mock(return_value=httpx.Response(201, json={'id': 2, 'url': ''}))
|
|
688
|
-
plugin = GitHubDeploymentPlugin()
|
|
689
|
-
ctx = _ctx(
|
|
690
|
-
environment='staging',
|
|
691
|
-
environment_config={'payload': 'not-a-dict'},
|
|
692
|
-
)
|
|
693
|
-
await plugin.trigger_deployment(
|
|
694
|
-
ctx, _CREDS, ref_or_sha='v1.0.0', inputs={'k': 'v'}
|
|
672
|
+
{'cluster': 'override', 'feature_flag': 'on', 'extra': 'kept'},
|
|
695
673
|
)
|
|
696
|
-
body = json.loads(deploy.calls.last.request.read())
|
|
697
|
-
self.assertEqual(body['payload'], {'k': 'v'})
|
|
698
674
|
|
|
699
675
|
|
|
700
676
|
class ListRefsPaginationTestCase(unittest.IsolatedAsyncioTestCase):
|
|
@@ -860,6 +836,257 @@ class GetDeploymentStatusTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
860
836
|
self.assertEqual(run.status, 'cancelled')
|
|
861
837
|
|
|
862
838
|
|
|
839
|
+
class ListRecentDeploymentsTestCase(unittest.IsolatedAsyncioTestCase):
|
|
840
|
+
@respx.mock
|
|
841
|
+
async def test_one_env_one_deployment_success(self) -> None:
|
|
842
|
+
respx.get(
|
|
843
|
+
'https://api.github.com/repos/octo/demo/deployments',
|
|
844
|
+
params={
|
|
845
|
+
'environment': 'infrastructure-testing',
|
|
846
|
+
'per_page': '1',
|
|
847
|
+
},
|
|
848
|
+
).mock(
|
|
849
|
+
return_value=httpx.Response(
|
|
850
|
+
200,
|
|
851
|
+
json=[
|
|
852
|
+
{
|
|
853
|
+
'id': 123,
|
|
854
|
+
'sha': '2668cd0abc',
|
|
855
|
+
'ref': 'main',
|
|
856
|
+
'created_at': '2026-05-13T14:00:00Z',
|
|
857
|
+
'description': 'Deploy main',
|
|
858
|
+
'url': 'https://api.github.com/repos/octo/demo/deployments/123',
|
|
859
|
+
}
|
|
860
|
+
],
|
|
861
|
+
)
|
|
862
|
+
)
|
|
863
|
+
respx.get(
|
|
864
|
+
'https://api.github.com/repos/octo/demo/deployments/123/statuses'
|
|
865
|
+
).mock(
|
|
866
|
+
return_value=httpx.Response(
|
|
867
|
+
200,
|
|
868
|
+
json=[
|
|
869
|
+
{
|
|
870
|
+
'state': 'success',
|
|
871
|
+
'log_url': 'https://gh/runs/9001',
|
|
872
|
+
'created_at': '2026-05-13T14:01:00Z',
|
|
873
|
+
}
|
|
874
|
+
],
|
|
875
|
+
)
|
|
876
|
+
)
|
|
877
|
+
plugin = GitHubDeploymentPlugin()
|
|
878
|
+
events = await plugin.list_recent_deployments(
|
|
879
|
+
_ctx(), _CREDS, ['infrastructure-testing']
|
|
880
|
+
)
|
|
881
|
+
self.assertEqual(len(events), 1)
|
|
882
|
+
event = events[0]
|
|
883
|
+
self.assertEqual(event.environment, 'infrastructure-testing')
|
|
884
|
+
self.assertEqual(event.sha, '2668cd0abc')
|
|
885
|
+
self.assertEqual(event.ref, 'main')
|
|
886
|
+
self.assertEqual(event.status, 'success')
|
|
887
|
+
self.assertEqual(event.external_run_id, '123')
|
|
888
|
+
self.assertEqual(event.run_url, 'https://gh/runs/9001')
|
|
889
|
+
self.assertEqual(
|
|
890
|
+
event.deployment_url,
|
|
891
|
+
'https://api.github.com/repos/octo/demo/deployments/123',
|
|
892
|
+
)
|
|
893
|
+
# ``created_at`` must come from the deployment row, not the
|
|
894
|
+
# latest status row (which is one minute later above).
|
|
895
|
+
self.assertEqual(
|
|
896
|
+
event.created_at,
|
|
897
|
+
datetime.datetime(2026, 5, 13, 14, 0, tzinfo=datetime.UTC),
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
@respx.mock
|
|
901
|
+
async def test_multiple_envs_fan_out_in_parallel(self) -> None:
|
|
902
|
+
respx.get(
|
|
903
|
+
'https://api.github.com/repos/octo/demo/deployments',
|
|
904
|
+
params={'environment': 'production', 'per_page': '1'},
|
|
905
|
+
).mock(
|
|
906
|
+
return_value=httpx.Response(
|
|
907
|
+
200,
|
|
908
|
+
json=[
|
|
909
|
+
{
|
|
910
|
+
'id': 1,
|
|
911
|
+
'sha': 'prodsha',
|
|
912
|
+
'ref': 'v1.0.0',
|
|
913
|
+
'created_at': '2026-05-13T12:00:00Z',
|
|
914
|
+
}
|
|
915
|
+
],
|
|
916
|
+
)
|
|
917
|
+
)
|
|
918
|
+
respx.get(
|
|
919
|
+
'https://api.github.com/repos/octo/demo/deployments',
|
|
920
|
+
params={'environment': 'staging', 'per_page': '1'},
|
|
921
|
+
).mock(
|
|
922
|
+
return_value=httpx.Response(
|
|
923
|
+
200,
|
|
924
|
+
json=[
|
|
925
|
+
{
|
|
926
|
+
'id': 2,
|
|
927
|
+
'sha': 'stagesha',
|
|
928
|
+
'ref': 'main',
|
|
929
|
+
'created_at': '2026-05-13T13:00:00Z',
|
|
930
|
+
}
|
|
931
|
+
],
|
|
932
|
+
)
|
|
933
|
+
)
|
|
934
|
+
# Both deployments resolve to ``pending`` because no statuses
|
|
935
|
+
# have been posted yet. The empty-statuses case must not be
|
|
936
|
+
# treated as an error.
|
|
937
|
+
respx.get(
|
|
938
|
+
'https://api.github.com/repos/octo/demo/deployments/1/statuses'
|
|
939
|
+
).mock(return_value=httpx.Response(200, json=[]))
|
|
940
|
+
respx.get(
|
|
941
|
+
'https://api.github.com/repos/octo/demo/deployments/2/statuses'
|
|
942
|
+
).mock(return_value=httpx.Response(200, json=[]))
|
|
943
|
+
plugin = GitHubDeploymentPlugin()
|
|
944
|
+
events = await plugin.list_recent_deployments(
|
|
945
|
+
_ctx(), _CREDS, ['production', 'staging']
|
|
946
|
+
)
|
|
947
|
+
by_env = {e.environment: e for e in events}
|
|
948
|
+
self.assertEqual(by_env['production'].external_run_id, '1')
|
|
949
|
+
self.assertEqual(by_env['staging'].external_run_id, '2')
|
|
950
|
+
self.assertEqual(by_env['production'].status, 'pending')
|
|
951
|
+
self.assertEqual(by_env['staging'].status, 'pending')
|
|
952
|
+
|
|
953
|
+
@respx.mock
|
|
954
|
+
async def test_unknown_env_skipped_not_raised(self) -> None:
|
|
955
|
+
# GitHub returns 404 for an environment the repo doesn't know
|
|
956
|
+
# about; resync must keep the partial result rather than fail.
|
|
957
|
+
respx.get(
|
|
958
|
+
'https://api.github.com/repos/octo/demo/deployments',
|
|
959
|
+
params={'environment': 'production', 'per_page': '1'},
|
|
960
|
+
).mock(
|
|
961
|
+
return_value=httpx.Response(
|
|
962
|
+
200,
|
|
963
|
+
json=[
|
|
964
|
+
{
|
|
965
|
+
'id': 7,
|
|
966
|
+
'sha': 'abc',
|
|
967
|
+
'created_at': '2026-05-13T14:00:00Z',
|
|
968
|
+
}
|
|
969
|
+
],
|
|
970
|
+
)
|
|
971
|
+
)
|
|
972
|
+
respx.get(
|
|
973
|
+
'https://api.github.com/repos/octo/demo/deployments',
|
|
974
|
+
params={'environment': 'never-deployed', 'per_page': '1'},
|
|
975
|
+
).mock(return_value=httpx.Response(404, json={'message': 'Not Found'}))
|
|
976
|
+
respx.get(
|
|
977
|
+
'https://api.github.com/repos/octo/demo/deployments/7/statuses'
|
|
978
|
+
).mock(return_value=httpx.Response(200, json=[]))
|
|
979
|
+
plugin = GitHubDeploymentPlugin()
|
|
980
|
+
events = await plugin.list_recent_deployments(
|
|
981
|
+
_ctx(), _CREDS, ['production', 'never-deployed']
|
|
982
|
+
)
|
|
983
|
+
self.assertEqual([e.environment for e in events], ['production'])
|
|
984
|
+
|
|
985
|
+
@respx.mock
|
|
986
|
+
async def test_inactive_status_maps_to_rolled_back(self) -> None:
|
|
987
|
+
respx.get('https://api.github.com/repos/octo/demo/deployments').mock(
|
|
988
|
+
return_value=httpx.Response(
|
|
989
|
+
200,
|
|
990
|
+
json=[
|
|
991
|
+
{
|
|
992
|
+
'id': 99,
|
|
993
|
+
'sha': 'old',
|
|
994
|
+
'created_at': '2026-05-01T00:00:00Z',
|
|
995
|
+
}
|
|
996
|
+
],
|
|
997
|
+
)
|
|
998
|
+
)
|
|
999
|
+
respx.get(
|
|
1000
|
+
'https://api.github.com/repos/octo/demo/deployments/99/statuses'
|
|
1001
|
+
).mock(
|
|
1002
|
+
return_value=httpx.Response(
|
|
1003
|
+
200,
|
|
1004
|
+
json=[{'state': 'inactive'}],
|
|
1005
|
+
)
|
|
1006
|
+
)
|
|
1007
|
+
plugin = GitHubDeploymentPlugin()
|
|
1008
|
+
events = await plugin.list_recent_deployments(
|
|
1009
|
+
_ctx(), _CREDS, ['staging']
|
|
1010
|
+
)
|
|
1011
|
+
self.assertEqual(events[0].status, 'rolled_back')
|
|
1012
|
+
|
|
1013
|
+
@respx.mock
|
|
1014
|
+
async def test_status_history_failure_maps_to_failed(self) -> None:
|
|
1015
|
+
respx.get('https://api.github.com/repos/octo/demo/deployments').mock(
|
|
1016
|
+
return_value=httpx.Response(
|
|
1017
|
+
200,
|
|
1018
|
+
json=[
|
|
1019
|
+
{
|
|
1020
|
+
'id': 5,
|
|
1021
|
+
'sha': 'abc',
|
|
1022
|
+
'created_at': '2026-05-01T00:00:00Z',
|
|
1023
|
+
}
|
|
1024
|
+
],
|
|
1025
|
+
)
|
|
1026
|
+
)
|
|
1027
|
+
respx.get(
|
|
1028
|
+
'https://api.github.com/repos/octo/demo/deployments/5/statuses'
|
|
1029
|
+
).mock(
|
|
1030
|
+
return_value=httpx.Response(
|
|
1031
|
+
200,
|
|
1032
|
+
json=[{'state': 'failure'}, {'state': 'in_progress'}],
|
|
1033
|
+
)
|
|
1034
|
+
)
|
|
1035
|
+
plugin = GitHubDeploymentPlugin()
|
|
1036
|
+
events = await plugin.list_recent_deployments(
|
|
1037
|
+
_ctx(), _CREDS, ['staging']
|
|
1038
|
+
)
|
|
1039
|
+
self.assertEqual(events[0].status, 'failed')
|
|
1040
|
+
|
|
1041
|
+
@respx.mock
|
|
1042
|
+
async def test_status_fetch_error_degrades_to_pending(self) -> None:
|
|
1043
|
+
respx.get('https://api.github.com/repos/octo/demo/deployments').mock(
|
|
1044
|
+
return_value=httpx.Response(
|
|
1045
|
+
200,
|
|
1046
|
+
json=[
|
|
1047
|
+
{
|
|
1048
|
+
'id': 8,
|
|
1049
|
+
'sha': 'abc',
|
|
1050
|
+
'created_at': '2026-05-01T00:00:00Z',
|
|
1051
|
+
}
|
|
1052
|
+
],
|
|
1053
|
+
)
|
|
1054
|
+
)
|
|
1055
|
+
respx.get(
|
|
1056
|
+
'https://api.github.com/repos/octo/demo/deployments/8/statuses'
|
|
1057
|
+
).mock(return_value=httpx.Response(500, json={'message': 'oops'}))
|
|
1058
|
+
plugin = GitHubDeploymentPlugin()
|
|
1059
|
+
events = await plugin.list_recent_deployments(
|
|
1060
|
+
_ctx(), _CREDS, ['staging']
|
|
1061
|
+
)
|
|
1062
|
+
self.assertEqual(events[0].status, 'pending')
|
|
1063
|
+
self.assertIsNone(events[0].run_url)
|
|
1064
|
+
|
|
1065
|
+
@respx.mock
|
|
1066
|
+
async def test_deployment_missing_id_skipped(self) -> None:
|
|
1067
|
+
respx.get('https://api.github.com/repos/octo/demo/deployments').mock(
|
|
1068
|
+
return_value=httpx.Response(
|
|
1069
|
+
200,
|
|
1070
|
+
json=[
|
|
1071
|
+
{'sha': 'abc'}, # missing id
|
|
1072
|
+
{
|
|
1073
|
+
'id': 11,
|
|
1074
|
+
'sha': 'def',
|
|
1075
|
+
'created_at': '2026-05-13T00:00:00Z',
|
|
1076
|
+
},
|
|
1077
|
+
],
|
|
1078
|
+
)
|
|
1079
|
+
)
|
|
1080
|
+
respx.get(
|
|
1081
|
+
'https://api.github.com/repos/octo/demo/deployments/11/statuses'
|
|
1082
|
+
).mock(return_value=httpx.Response(200, json=[]))
|
|
1083
|
+
plugin = GitHubDeploymentPlugin()
|
|
1084
|
+
events = await plugin.list_recent_deployments(
|
|
1085
|
+
_ctx(), _CREDS, ['staging']
|
|
1086
|
+
)
|
|
1087
|
+
self.assertEqual([e.external_run_id for e in events], ['11'])
|
|
1088
|
+
|
|
1089
|
+
|
|
863
1090
|
class CheckStatusTestCase(unittest.IsolatedAsyncioTestCase):
|
|
864
1091
|
@respx.mock
|
|
865
1092
|
async def test_check_status_pass(self) -> None:
|
|
@@ -245,25 +245,33 @@ wheels = [
|
|
|
245
245
|
|
|
246
246
|
[[package]]
|
|
247
247
|
name = "clickhouse-connect"
|
|
248
|
-
version = "0.
|
|
248
|
+
version = "1.0.0"
|
|
249
249
|
source = { registry = "https://pypi.org/simple" }
|
|
250
250
|
dependencies = [
|
|
251
251
|
{ name = "certifi" },
|
|
252
252
|
{ name = "lz4" },
|
|
253
|
-
{ name = "
|
|
253
|
+
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
|
254
254
|
{ name = "urllib3" },
|
|
255
255
|
{ name = "zstandard" },
|
|
256
256
|
]
|
|
257
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
258
|
-
wheels = [
|
|
259
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
260
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
261
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
262
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
263
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
264
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
265
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
266
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
257
|
+
sdist = { url = "https://files.pythonhosted.org/packages/9c/65/4358da2a8b20af0f8b01895e420870ad969afc7b197be0c9af35811b4329/clickhouse_connect-1.0.0.tar.gz", hash = "sha256:0d4ed1e296c6e77dc0dbf486607cc39fc6e36866abb653543d5ad0a8426ea8f0", size = 140624, upload-time = "2026-05-14T14:47:08.669Z" }
|
|
258
|
+
wheels = [
|
|
259
|
+
{ url = "https://files.pythonhosted.org/packages/ad/75/37f05df90fbb5f1bcc12be92be9e19e7d07a09ae23082fe4ade41f2459d1/clickhouse_connect-1.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:20b76e54c9ea77e86c47b3194f1a9ff1833bd6fa34cb81bfcec621eff94ed033", size = 315197, upload-time = "2026-05-14T14:46:38.385Z" },
|
|
260
|
+
{ url = "https://files.pythonhosted.org/packages/4b/6a/8c99f92357e2901e7e6d322e64949ceb4355910522f7fb4e35c5e2861519/clickhouse_connect-1.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0fcccd2b49d57a09e465fc0c01c7c323f0db637241b3fc93d869d1b256c46184", size = 304704, upload-time = "2026-05-14T14:46:39.522Z" },
|
|
261
|
+
{ url = "https://files.pythonhosted.org/packages/ed/f9/8830d8fc3100113263da1cf7a8e7d6de74a2f3770f0b26313d4ca4cc78b4/clickhouse_connect-1.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:295e6ff6e3f7fe71c28b1753967d8669ce12d924486150c74c892d8f67cba4d9", size = 1225573, upload-time = "2026-05-14T14:46:41.122Z" },
|
|
262
|
+
{ url = "https://files.pythonhosted.org/packages/1d/39/3935cb0cd6858e29eb38d3bc49bf090501a164b44b9f71e3b4889dff363f/clickhouse_connect-1.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e74375b92cfa94cb4c944a393a027af5c5ed562fc6b862baf6288696bc48688", size = 1239054, upload-time = "2026-05-14T14:46:42.493Z" },
|
|
263
|
+
{ url = "https://files.pythonhosted.org/packages/5d/6d/48b8666bb47b13d32b80298d20110111ca5974d6e4dab56ba83f22740319/clickhouse_connect-1.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dae66adb575b7eb9626d84edad4481d5c3d93ca680766c3f1891c13694913bee", size = 1205056, upload-time = "2026-05-14T14:46:44.052Z" },
|
|
264
|
+
{ url = "https://files.pythonhosted.org/packages/76/48/2f2500fdbb33748c159c070084e1924c0d86b3c2efc1316f3f952e872eda/clickhouse_connect-1.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1415f440ae59b03e10bd5cc11c0034f30ad5f8fad74b42153926482c99ac21eb", size = 1237099, upload-time = "2026-05-14T14:46:45.387Z" },
|
|
265
|
+
{ url = "https://files.pythonhosted.org/packages/3b/8a/03b4daf7a8b40a137a62972a57875793b50769af794a7c5f9da68d4c0234/clickhouse_connect-1.0.0-cp314-cp314-win32.whl", hash = "sha256:6977afeabca17cdd3bc11215902f20e850eb1d71451d93eaee8ac26068a84af4", size = 285043, upload-time = "2026-05-14T14:46:46.787Z" },
|
|
266
|
+
{ url = "https://files.pythonhosted.org/packages/bb/3e/2c230465c033833554f741c18a3db9c97b3c91da147c9903b7ef7779b8d3/clickhouse_connect-1.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:29185891d5de03f60ff9da37dbe1e1e44bed8ef252d0d1281ddf3a450dfe5023", size = 304303, upload-time = "2026-05-14T14:46:47.97Z" },
|
|
267
|
+
{ url = "https://files.pythonhosted.org/packages/7c/78/480f06e28c859cf9eef88e1297b936f78011258b8fd97d7dfe9307533fa1/clickhouse_connect-1.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:3222ae6bf39930a34ff4a483c6b34a1f3dab02d6743428b0383f4d47af7a48db", size = 333380, upload-time = "2026-05-14T14:46:49.158Z" },
|
|
268
|
+
{ url = "https://files.pythonhosted.org/packages/c3/7a/f79bde20e8fd4c620eccdcd4cff0ef68d68394e857d16e3788db324a6901/clickhouse_connect-1.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ec2a93d353415951ab10da943f30f0038acc0a1ff82b3253d82a106f3065f34", size = 325717, upload-time = "2026-05-14T14:46:50.715Z" },
|
|
269
|
+
{ url = "https://files.pythonhosted.org/packages/87/ab/ac02a65cab5fb3d734218b9f4171de15ee7105fb180ea7ce7d2632ca6d4c/clickhouse_connect-1.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:320f0c53c4cb8b06732d875cabd35a357992a9e0fcf9f4a18b631ea7ce377ef1", size = 1316892, upload-time = "2026-05-14T14:46:51.905Z" },
|
|
270
|
+
{ url = "https://files.pythonhosted.org/packages/20/8e/cb2538d6857d2906bd07f07ab0e1e4080f5ec47eab7fe42432ba476dc59a/clickhouse_connect-1.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ab4bda888575a2ea882ad4ed05bf9f5f15bb621209f6a4045231e3f26fedf7", size = 1297570, upload-time = "2026-05-14T14:46:53.673Z" },
|
|
271
|
+
{ url = "https://files.pythonhosted.org/packages/8e/8f/10318294e345e4ad1ad7e467fff56677b1e18617227056aa9bbea7949c8f/clickhouse_connect-1.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c4f5c8fa36b605435b32efea4f40ce11babb5f5afcec1e00f6a6cce81f81000", size = 1273741, upload-time = "2026-05-14T14:46:55.424Z" },
|
|
272
|
+
{ url = "https://files.pythonhosted.org/packages/cd/be/49c1acf41ef880f24ae69f2387ec5b7428496f97c4ce8863d1dfe824352b/clickhouse_connect-1.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:046f79215ebdd62192cc36e4d81b549570ca46e5b61e107b0d54cf7855b2ffff", size = 1288667, upload-time = "2026-05-14T14:46:56.948Z" },
|
|
273
|
+
{ url = "https://files.pythonhosted.org/packages/21/21/c1aa0db7edc073b5aaa6aeae7f24ca3a1cad584516556c6bd5cccc3cf6fd/clickhouse_connect-1.0.0-cp314-cp314t-win32.whl", hash = "sha256:9c64f7a26f97eeca472dab6ca9baddc1e36fae1d1c244541926ada4fad14a99b", size = 309551, upload-time = "2026-05-14T14:46:58.449Z" },
|
|
274
|
+
{ url = "https://files.pythonhosted.org/packages/3c/17/3d1f583075ef5e10cc45ba4c2b2f9533cc5fed45c5371aaf290df130bc80/clickhouse_connect-1.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:53f1f9cd31b1af371340f39923a08ba0aef57a7a85ef4a27376107f376c1efc3", size = 336112, upload-time = "2026-05-14T14:46:59.615Z" },
|
|
267
275
|
]
|
|
268
276
|
|
|
269
277
|
[package.optional-dependencies]
|
|
@@ -644,10 +652,12 @@ wheels = [
|
|
|
644
652
|
|
|
645
653
|
[[package]]
|
|
646
654
|
name = "imbi-common"
|
|
647
|
-
version = "
|
|
655
|
+
version = "2.5.5"
|
|
648
656
|
source = { registry = "https://pypi.org/simple" }
|
|
649
657
|
dependencies = [
|
|
650
658
|
{ name = "cryptography" },
|
|
659
|
+
{ name = "httpx" },
|
|
660
|
+
{ name = "jsonpointer" },
|
|
651
661
|
{ name = "jsonschema-models" },
|
|
652
662
|
{ name = "markdown-it-py" },
|
|
653
663
|
{ name = "nanoid" },
|
|
@@ -658,9 +668,9 @@ dependencies = [
|
|
|
658
668
|
{ name = "python-slugify" },
|
|
659
669
|
{ name = "typer" },
|
|
660
670
|
]
|
|
661
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
671
|
+
sdist = { url = "https://files.pythonhosted.org/packages/0d/b7/6dab7155b281f9c075d3434e3f234ef217fe49db840b14e59e3e5a75faa2/imbi_common-2.5.5.tar.gz", hash = "sha256:56751efe31fdc1fcb1d1be19e6b3f073d5b3e4fda3c89c0bcdec5c7943549477", size = 277481, upload-time = "2026-05-20T02:10:09.044Z" }
|
|
662
672
|
wheels = [
|
|
663
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
673
|
+
{ url = "https://files.pythonhosted.org/packages/48/04/69e72ec3a1968a9dd8b91df38e6a7be16e1399a054a953777a23f34c60ca/imbi_common-2.5.5-py3-none-any.whl", hash = "sha256:19ffa23d9cdb0b8065af1d3a9166f819c15414fc4b5707d089adaf64b932ebf1", size = 91154, upload-time = "2026-05-20T02:10:07.448Z" },
|
|
664
674
|
]
|
|
665
675
|
|
|
666
676
|
[package.optional-dependencies]
|
|
@@ -678,7 +688,7 @@ server = [
|
|
|
678
688
|
|
|
679
689
|
[[package]]
|
|
680
690
|
name = "imbi-plugin-github"
|
|
681
|
-
version = "1.
|
|
691
|
+
version = "1.2.0"
|
|
682
692
|
source = { editable = "." }
|
|
683
693
|
dependencies = [
|
|
684
694
|
{ name = "httpx" },
|
|
@@ -706,7 +716,7 @@ dist = [
|
|
|
706
716
|
[package.metadata]
|
|
707
717
|
requires-dist = [
|
|
708
718
|
{ name = "httpx", specifier = ">=0.27" },
|
|
709
|
-
{ name = "imbi-common", specifier = ">=
|
|
719
|
+
{ name = "imbi-common", specifier = ">=2.5.5" },
|
|
710
720
|
{ name = "pydantic", specifier = ">=2" },
|
|
711
721
|
]
|
|
712
722
|
|
|
@@ -778,6 +788,15 @@ wheels = [
|
|
|
778
788
|
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
|
|
779
789
|
]
|
|
780
790
|
|
|
791
|
+
[[package]]
|
|
792
|
+
name = "jsonpointer"
|
|
793
|
+
version = "3.1.1"
|
|
794
|
+
source = { registry = "https://pypi.org/simple" }
|
|
795
|
+
sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" }
|
|
796
|
+
wheels = [
|
|
797
|
+
{ url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" },
|
|
798
|
+
]
|
|
799
|
+
|
|
781
800
|
[[package]]
|
|
782
801
|
name = "jsonschema-models"
|
|
783
802
|
version = "1.2.3"
|
|
@@ -1467,15 +1486,6 @@ wheels = [
|
|
|
1467
1486
|
{ url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" },
|
|
1468
1487
|
]
|
|
1469
1488
|
|
|
1470
|
-
[[package]]
|
|
1471
|
-
name = "pytz"
|
|
1472
|
-
version = "2026.2"
|
|
1473
|
-
source = { registry = "https://pypi.org/simple" }
|
|
1474
|
-
sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" }
|
|
1475
|
-
wheels = [
|
|
1476
|
-
{ url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" },
|
|
1477
|
-
]
|
|
1478
|
-
|
|
1479
1489
|
[[package]]
|
|
1480
1490
|
name = "pywin32-ctypes"
|
|
1481
1491
|
version = "0.2.3"
|
|
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
|