diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..b46a1f9
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+if [[ $(type -t use_flake) != function ]]; then
+ echo "ERROR: direnv's use_flake function missing. update direnv to v2.30.0 or later." && exit 1
+fi
+#export BOT_ENV_FILE=./dev.env
+export BOT_CONFIG_FILE=config.json
+use flake
+dotenv
diff --git a/.gitignore b/.gitignore
index b074105..ea09b8c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ __pycache__
.venv
devstate
.env*
+!.envrc
config.json
dev.data
data
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index 31709be..0000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,15 +0,0 @@
----
-image: python:3.11
-
-stages:
- - check
-
-check:
- image: python:3.11-bookworm
- stage: check
- script:
- - apt-get update
- - apt-get install -y make libolm-dev
- - pip install uv
- - uv sync --frozen --group dev
- - uv run make check
diff --git a/README.md b/README.md
index 6155819..6b04a51 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,8 @@
> a bot for ops in matrix
+Repository: https://guardianproject.dev/ops/matrix-ops-bot
+
This bot catches webhooks and forwards them as messages to matrix rooms.
Current supported webhooks:
diff --git a/flake.nix b/flake.nix
index f58173d..60a79a5 100644
--- a/flake.nix
+++ b/flake.nix
@@ -172,10 +172,53 @@
let
system = pkgs.stdenv.hostPlatform.system;
exportedPackage = self.packages.${system}.default;
+ testVenv = exportedPackage.testVenv;
+ src = ./.;
+ ruffCheck = pkgs.stdenv.mkDerivation {
+ name = "matrix-ops-bot-ruff";
+ inherit src;
+ dontConfigure = true;
+ dontBuild = true;
+ nativeBuildInputs = [ testVenv ];
+ checkPhase = ''
+ runHook preCheck
+ ruff check ops_bot/ tests/
+ ruff format --check ops_bot/ tests/
+ runHook postCheck
+ '';
+ doCheck = true;
+ installPhase = ''
+ mkdir -p "$out"
+ touch "$out/passed"
+ '';
+ };
+ pyrightCheck = pkgs.stdenv.mkDerivation {
+ name = "matrix-ops-bot-pyright";
+ inherit src;
+ dontConfigure = true;
+ dontBuild = true;
+ nativeBuildInputs = [
+ testVenv
+ pkgs.nodejs
+ ];
+ checkPhase = ''
+ runHook preCheck
+ export HOME="$(mktemp -d)"
+ pyright ops_bot/
+ runHook postCheck
+ '';
+ doCheck = true;
+ installPhase = ''
+ mkdir -p "$out"
+ touch "$out/passed"
+ '';
+ };
in
{
package-default = exportedPackage;
tests = exportedPackage.tests;
+ ruff = ruffCheck;
+ pyright = pyrightCheck;
}
);
@@ -184,6 +227,8 @@
packages = [
pkgs.python311
pkgs.uv
+ pkgs.ruff
+ pkgs.pyright
];
env.LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
pkgs.stdenv.cc.cc
diff --git a/ops_bot/aws.py b/ops_bot/aws.py
index 5f2d7e7..ed8a1c6 100644
--- a/ops_bot/aws.py
+++ b/ops_bot/aws.py
@@ -92,7 +92,7 @@ def handle_cloudtrail_sts(payload: Any) -> List[Tuple[str, str]]:
for x in [
f"**🚨 ALERT[{subject}]** : {title}",
f"- **Region**: {region}",
- f"- **Assumed Role**: {assumed_role}",
+ (f"- **Assumed Role**: {assumed_role}" if assumed_role else None),
f"- **Event Time**: {event_time}",
f"- **Account ID**: {account_id}",
]
diff --git a/ops_bot/gitlab/hook.py b/ops_bot/gitlab/hook.py
index b4c7a70..7d7609d 100644
--- a/ops_bot/gitlab/hook.py
+++ b/ops_bot/gitlab/hook.py
@@ -1,6 +1,6 @@
import logging
import re
-from typing import Any, List, Optional, Tuple
+from typing import Any, List, Optional, Tuple, cast
import attr
from fastapi import Request
@@ -47,8 +47,9 @@ async def handle_event(x_gitlab_event: str, payload: Any) -> List[Tuple[str, str
nonlocal aborted
aborted = True
+ action_fields = cast(Any, Action)
base_args = {
- **{field.key: field for field in Action if field.key.isupper()},
+ **{field.key: field for field in action_fields if field.key.isupper()},
**OTHER_ENUMS,
"util": TemplateUtil,
}
diff --git a/ops_bot/gitlab/types.py b/ops_bot/gitlab/types.py
index 63d23a3..524a92c 100644
--- a/ops_bot/gitlab/types.py
+++ b/ops_bot/gitlab/types.py
@@ -717,7 +717,7 @@ class GitlabPushEvent(SerializableAttrs, GitlabEvent):
def split_updates(
- evt: Union["GitlabIssueEvent", "GitlabMergeRequestEvent"]
+ evt: Union["GitlabIssueEvent", "GitlabMergeRequestEvent"],
) -> List[GitlabEvent]:
if not evt.changes:
return [evt]
diff --git a/ops_bot/main.py b/ops_bot/main.py
index ec43602..44316c4 100644
--- a/ops_bot/main.py
+++ b/ops_bot/main.py
@@ -14,7 +14,6 @@ from fastapi.security import (
HTTPBasicCredentials,
HTTPBearer,
)
-from prometheus_client import start_http_server
from prometheus_fastapi_instrumentator import Instrumentator
from ops_bot import alertmanager, aws, pagerduty
@@ -101,8 +100,7 @@ class Authorizer(Protocol):
request: Request,
basic_credentials: Optional[HTTPBasicCredentials],
bearer_credentials: Optional[HTTPAuthorizationCredentials],
- ) -> bool:
- ...
+ ) -> bool: ...
class ParseHandler(Protocol):
@@ -111,8 +109,7 @@ class ParseHandler(Protocol):
route: RoutingKey,
payload: Any,
request: Request,
- ) -> List[Tuple[str, str]]:
- ...
+ ) -> List[Tuple[str, str]]: ...
handlers: Dict[str, Tuple[Authorizer, ParseHandler]] = {
diff --git a/ops_bot/matrix.py b/ops_bot/matrix.py
index fcc4fb4..a2a5e39 100644
--- a/ops_bot/matrix.py
+++ b/ops_bot/matrix.py
@@ -21,9 +21,11 @@ class ClientCredentials(BaseModel):
class CredentialStorage(Protocol):
def save_config(self, config: ClientCredentials) -> None:
"""Save config"""
+ ...
def read_config(self) -> ClientCredentials:
"""Load config"""
+ ...
class LocalCredentialStore:
@@ -64,7 +66,7 @@ class MatrixClient:
self.store_path.joinpath("credentials.json")
)
- self.client: AsyncClient = None
+ self.client: Optional[AsyncClient] = None
self.client_config = AsyncClientConfig(
max_limit_exceeded=0,
max_timeouts=0,
@@ -79,15 +81,19 @@ class MatrixClient:
async def start(self) -> None:
await self.login()
+ if self.client is None:
+ raise RuntimeError("Matrix client failed to initialize")
- if self.client.should_upload_keys:
- await self.client.keys_upload()
+ client = self.client
+
+ if client.should_upload_keys:
+ await client.keys_upload()
for room in self.join_rooms:
- await self.client.join(room)
+ await client.join(room)
- await self.client.joined_rooms()
- await self.client.sync_forever(timeout=300000, full_state=True)
+ await client.joined_rooms()
+ await client.sync_forever(timeout=300000, full_state=True)
def save_credentials(self, resp: LoginResponse, homeserver: str) -> None:
credentials = ClientCredentials(
@@ -150,6 +156,9 @@ class MatrixClient:
message: str,
message_formatted: Optional[str] = None,
) -> None:
+ if self.client is None:
+ raise RuntimeError("Matrix client failed to initialize")
+
content = {
"msgtype": "m.text",
"body": f"{message}",
@@ -168,4 +177,5 @@ class MatrixClient:
)
async def shutdown(self) -> None:
- await self.client.close()
+ if self.client is not None:
+ await self.client.close()
diff --git a/ops_bot/util/template.py b/ops_bot/util/template.py
index 4efb18a..52c72b0 100644
--- a/ops_bot/util/template.py
+++ b/ops_bot/util/template.py
@@ -123,14 +123,14 @@ class PluginTemplateLoader(BaseLoader):
self.macros = sync_read_file(base_path / "macros.html")
def get_source(
- self, environment: Any, name: str
+ self, environment: Any, template: str
) -> Tuple[str, str, Callable[[], bool]]:
- path = self.directory / f"{name}.html"
+ path = self.directory / f"{template}.html"
try:
tpl = sync_read_file(path)
except KeyError:
- raise TemplateNotFound(name)
- return self.macros + tpl, name, lambda: True
+ raise TemplateNotFound(template)
+ return self.macros + tpl, template, lambda: True
def list_templates(self) -> List[str]:
return [
diff --git a/pyproject.toml b/pyproject.toml
index ef3df27..c0f4c2e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,6 +30,8 @@ matrix-ops-bot = "ops_bot.main:main"
[dependency-groups]
dev = [
"pytest>=7.2.0,<8.0.0",
+ "ruff>=0.9.0,<1.0.0",
+ "pyright>=1.1.390,<2.0.0",
"black>=22.10.0,<23.0.0",
"isort>=5.10.1,<6.0.0",
"mypy>=1.2.0,<2.0.0",
diff --git a/shell.nix b/shell.nix
deleted file mode 100644
index 4c0d8c7..0000000
--- a/shell.nix
+++ /dev/null
@@ -1,23 +0,0 @@
-{ system ? "x86_64-linux", pkgs ? import { inherit system; } }:
-
-let
- packages = [
- pkgs.python313
- pkgs.uv
- pkgs.zsh
- pkgs.olm
- ];
-
- LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [
- pkgs.stdenv.cc.cc
- ];
-in
-pkgs.mkShell {
- buildInputs = packages;
- shellHook = ''
- export SHELL=${pkgs.zsh}
- export LD_LIBRARY_PATH="${LD_LIBRARY_PATH}"
- export UV_PROJECT_ENVIRONMENT=".venv"
- export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
- '';
-}
diff --git a/uv.lock b/uv.lock
index cb321f1..1b3c7eb 100644
--- a/uv.lock
+++ b/uv.lock
@@ -813,8 +813,10 @@ dev = [
{ name = "flake8-black" },
{ name = "isort" },
{ name = "mypy" },
+ { name = "pyright" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
+ { name = "ruff" },
{ name = "types-commonmark" },
{ name = "types-markdown" },
{ name = "types-termcolor" },
@@ -846,8 +848,10 @@ dev = [
{ name = "flake8-black", specifier = ">=0.3.5,<0.4.0" },
{ name = "isort", specifier = ">=5.10.1,<6.0.0" },
{ name = "mypy", specifier = ">=1.2.0,<2.0.0" },
+ { name = "pyright", specifier = ">=1.1.390,<2.0.0" },
{ name = "pytest", specifier = ">=7.2.0,<8.0.0" },
{ name = "pytest-asyncio", specifier = ">=0.20.2,<0.21.0" },
+ { name = "ruff", specifier = ">=0.9.0,<1.0.0" },
{ name = "types-commonmark", specifier = ">=0.9.2,<0.10.0" },
{ name = "types-markdown", specifier = ">=3.4.0,<4.0.0" },
{ name = "types-termcolor", specifier = ">=1.1.5,<2.0.0" },
@@ -1050,6 +1054,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
+[[package]]
+name = "nodeenv"
+version = "1.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
+]
+
[[package]]
name = "packaging"
version = "26.0"
@@ -1408,6 +1421,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
+[[package]]
+name = "pyright"
+version = "1.1.408"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nodeenv" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" },
+]
+
[[package]]
name = "pytest"
version = "7.4.4"
@@ -1660,6 +1686,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
]
+[[package]]
+name = "ruff"
+version = "0.15.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" },
+ { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" },
+ { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" },
+]
+
[[package]]
name = "sniffio"
version = "1.3.1"