bot-cmder 0.2.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 (170) hide show
  1. bot_cmder-0.2.0/.dockerignore +69 -0
  2. bot_cmder-0.2.0/.env.example +161 -0
  3. bot_cmder-0.2.0/.github/dependabot.yml +41 -0
  4. bot_cmder-0.2.0/.github/workflows/beta.yml +100 -0
  5. bot_cmder-0.2.0/.github/workflows/ci.yml +200 -0
  6. bot_cmder-0.2.0/.github/workflows/docker.yml +162 -0
  7. bot_cmder-0.2.0/.github/workflows/release.yml +92 -0
  8. bot_cmder-0.2.0/.gitignore +24 -0
  9. bot_cmder-0.2.0/.gitleaks.toml +143 -0
  10. bot_cmder-0.2.0/.pre-commit-config.yaml +40 -0
  11. bot_cmder-0.2.0/AGENTS.md +160 -0
  12. bot_cmder-0.2.0/CHANGELOG.md +154 -0
  13. bot_cmder-0.2.0/Dockerfile +106 -0
  14. bot_cmder-0.2.0/Justfile +156 -0
  15. bot_cmder-0.2.0/LICENSE +21 -0
  16. bot_cmder-0.2.0/PKG-INFO +151 -0
  17. bot_cmder-0.2.0/README.md +125 -0
  18. bot_cmder-0.2.0/bot_cmder/__init__.py +10 -0
  19. bot_cmder-0.2.0/bot_cmder/__main__.py +15 -0
  20. bot_cmder-0.2.0/bot_cmder/adapters/__init__.py +0 -0
  21. bot_cmder-0.2.0/bot_cmder/adapters/base.py +23 -0
  22. bot_cmder-0.2.0/bot_cmder/adapters/discord/__init__.py +5 -0
  23. bot_cmder-0.2.0/bot_cmder/adapters/discord/adapter.py +203 -0
  24. bot_cmder-0.2.0/bot_cmder/adapters/discord/client.py +126 -0
  25. bot_cmder-0.2.0/bot_cmder/adapters/discord/gateway.py +483 -0
  26. bot_cmder-0.2.0/bot_cmder/adapters/discord/router.py +122 -0
  27. bot_cmder-0.2.0/bot_cmder/adapters/discord/schemas.py +151 -0
  28. bot_cmder-0.2.0/bot_cmder/adapters/slack/__init__.py +5 -0
  29. bot_cmder-0.2.0/bot_cmder/adapters/slack/adapter.py +143 -0
  30. bot_cmder-0.2.0/bot_cmder/adapters/slack/client.py +89 -0
  31. bot_cmder-0.2.0/bot_cmder/adapters/slack/router.py +149 -0
  32. bot_cmder-0.2.0/bot_cmder/adapters/slack/schemas.py +59 -0
  33. bot_cmder-0.2.0/bot_cmder/adapters/slack/signing.py +88 -0
  34. bot_cmder-0.2.0/bot_cmder/adapters/slack/socket.py +254 -0
  35. bot_cmder-0.2.0/bot_cmder/adapters/telegram/__init__.py +5 -0
  36. bot_cmder-0.2.0/bot_cmder/adapters/telegram/adapter.py +54 -0
  37. bot_cmder-0.2.0/bot_cmder/adapters/telegram/client.py +153 -0
  38. bot_cmder-0.2.0/bot_cmder/adapters/telegram/daemon.py +201 -0
  39. bot_cmder-0.2.0/bot_cmder/adapters/telegram/router.py +63 -0
  40. bot_cmder-0.2.0/bot_cmder/adapters/telegram/schemas.py +43 -0
  41. bot_cmder-0.2.0/bot_cmder/audit/__init__.py +0 -0
  42. bot_cmder-0.2.0/bot_cmder/audit/log.py +240 -0
  43. bot_cmder-0.2.0/bot_cmder/auth/__init__.py +0 -0
  44. bot_cmder-0.2.0/bot_cmder/auth/acl.py +44 -0
  45. bot_cmder-0.2.0/bot_cmder/auth/emergency.py +173 -0
  46. bot_cmder-0.2.0/bot_cmder/auth/lockout.py +202 -0
  47. bot_cmder-0.2.0/bot_cmder/auth/lockout_store.py +168 -0
  48. bot_cmder-0.2.0/bot_cmder/auth/migrations/0001_initial.sql +10 -0
  49. bot_cmder-0.2.0/bot_cmder/auth/migrations/0002_lockout.sql +37 -0
  50. bot_cmder-0.2.0/bot_cmder/auth/migrations/__init__.py +0 -0
  51. bot_cmder-0.2.0/bot_cmder/auth/pending.py +77 -0
  52. bot_cmder-0.2.0/bot_cmder/auth/secret_store.py +124 -0
  53. bot_cmder-0.2.0/bot_cmder/auth/totp.py +65 -0
  54. bot_cmder-0.2.0/bot_cmder/cli/__init__.py +59 -0
  55. bot_cmder-0.2.0/bot_cmder/cli/discord_register.py +269 -0
  56. bot_cmder-0.2.0/bot_cmder/cli/init_cmd.py +178 -0
  57. bot_cmder-0.2.0/bot_cmder/cli/keys.py +38 -0
  58. bot_cmder-0.2.0/bot_cmder/cli/serve.py +118 -0
  59. bot_cmder-0.2.0/bot_cmder/cli/slack_manifest.py +226 -0
  60. bot_cmder-0.2.0/bot_cmder/cli/totp.py +157 -0
  61. bot_cmder-0.2.0/bot_cmder/commands/__init__.py +0 -0
  62. bot_cmder-0.2.0/bot_cmder/commands/builtin/__init__.py +104 -0
  63. bot_cmder-0.2.0/bot_cmder/commands/builtin/health.py +70 -0
  64. bot_cmder-0.2.0/bot_cmder/commands/builtin/help.py +28 -0
  65. bot_cmder-0.2.0/bot_cmder/commands/builtin/kubectl.py +68 -0
  66. bot_cmder-0.2.0/bot_cmder/commands/builtin/otp.py +498 -0
  67. bot_cmder-0.2.0/bot_cmder/commands/builtin/runbook.py +123 -0
  68. bot_cmder-0.2.0/bot_cmder/commands/builtin/service.py +388 -0
  69. bot_cmder-0.2.0/bot_cmder/commands/builtin/ssh.py +129 -0
  70. bot_cmder-0.2.0/bot_cmder/commands/builtin/whoami.py +28 -0
  71. bot_cmder-0.2.0/bot_cmder/config/__init__.py +0 -0
  72. bot_cmder-0.2.0/bot_cmder/config/paths.py +106 -0
  73. bot_cmder-0.2.0/bot_cmder/config/schema.py +365 -0
  74. bot_cmder-0.2.0/bot_cmder/config/settings.py +218 -0
  75. bot_cmder-0.2.0/bot_cmder/connectors/__init__.py +0 -0
  76. bot_cmder-0.2.0/bot_cmder/connectors/base.py +39 -0
  77. bot_cmder-0.2.0/bot_cmder/connectors/local.py +73 -0
  78. bot_cmder-0.2.0/bot_cmder/connectors/ssh.py +203 -0
  79. bot_cmder-0.2.0/bot_cmder/core/__init__.py +0 -0
  80. bot_cmder-0.2.0/bot_cmder/core/context.py +37 -0
  81. bot_cmder-0.2.0/bot_cmder/core/dispatcher.py +209 -0
  82. bot_cmder-0.2.0/bot_cmder/core/errors.py +22 -0
  83. bot_cmder-0.2.0/bot_cmder/core/events.py +82 -0
  84. bot_cmder-0.2.0/bot_cmder/core/parser.py +47 -0
  85. bot_cmder-0.2.0/bot_cmder/core/redact.py +81 -0
  86. bot_cmder-0.2.0/bot_cmder/core/registry.py +213 -0
  87. bot_cmder-0.2.0/bot_cmder/data/__init__.py +8 -0
  88. bot_cmder-0.2.0/bot_cmder/data/app.yaml.example +317 -0
  89. bot_cmder-0.2.0/bot_cmder/main.py +440 -0
  90. bot_cmder-0.2.0/bot_cmder/storage/__init__.py +0 -0
  91. bot_cmder-0.2.0/bot_cmder/storage/migrator.py +178 -0
  92. bot_cmder-0.2.0/docs/audit-rotation.md +169 -0
  93. bot_cmder-0.2.0/docs/discord-gateway.md +182 -0
  94. bot_cmder-0.2.0/docs/discord-setup.md +440 -0
  95. bot_cmder-0.2.0/docs/docker.md +327 -0
  96. bot_cmder-0.2.0/docs/getting-started.md +259 -0
  97. bot_cmder-0.2.0/docs/git-leak-prevention.md +273 -0
  98. bot_cmder-0.2.0/docs/otp.md +256 -0
  99. bot_cmder-0.2.0/docs/release.md +248 -0
  100. bot_cmder-0.2.0/docs/slack-setup.md +358 -0
  101. bot_cmder-0.2.0/docs/slack-socket-mode.md +159 -0
  102. bot_cmder-0.2.0/docs/telegram-polling.md +141 -0
  103. bot_cmder-0.2.0/pyproject.toml +99 -0
  104. bot_cmder-0.2.0/scripts/hook_status.py +42 -0
  105. bot_cmder-0.2.0/scripts/show_env_settings.py +104 -0
  106. bot_cmder-0.2.0/scripts/tunnel.sh +149 -0
  107. bot_cmder-0.2.0/scripts/tunnel_ngrok.sh +162 -0
  108. bot_cmder-0.2.0/tests/__init__.py +0 -0
  109. bot_cmder-0.2.0/tests/adapters/__init__.py +0 -0
  110. bot_cmder-0.2.0/tests/adapters/discord/__init__.py +0 -0
  111. bot_cmder-0.2.0/tests/adapters/discord/test_adapter.py +272 -0
  112. bot_cmder-0.2.0/tests/adapters/discord/test_client.py +82 -0
  113. bot_cmder-0.2.0/tests/adapters/discord/test_gateway.py +453 -0
  114. bot_cmder-0.2.0/tests/adapters/discord/test_router.py +157 -0
  115. bot_cmder-0.2.0/tests/adapters/slack/__init__.py +0 -0
  116. bot_cmder-0.2.0/tests/adapters/slack/test_adapter.py +270 -0
  117. bot_cmder-0.2.0/tests/adapters/slack/test_client.py +64 -0
  118. bot_cmder-0.2.0/tests/adapters/slack/test_router.py +249 -0
  119. bot_cmder-0.2.0/tests/adapters/slack/test_signing.py +121 -0
  120. bot_cmder-0.2.0/tests/adapters/slack/test_socket.py +316 -0
  121. bot_cmder-0.2.0/tests/adapters/telegram/__init__.py +0 -0
  122. bot_cmder-0.2.0/tests/adapters/telegram/test_adapter.py +66 -0
  123. bot_cmder-0.2.0/tests/adapters/telegram/test_client.py +157 -0
  124. bot_cmder-0.2.0/tests/adapters/telegram/test_daemon.py +233 -0
  125. bot_cmder-0.2.0/tests/adapters/telegram/test_router.py +95 -0
  126. bot_cmder-0.2.0/tests/adapters/telegram/test_set_my_commands.py +45 -0
  127. bot_cmder-0.2.0/tests/audit/__init__.py +0 -0
  128. bot_cmder-0.2.0/tests/audit/test_log.py +46 -0
  129. bot_cmder-0.2.0/tests/audit/test_log_rotation.py +355 -0
  130. bot_cmder-0.2.0/tests/auth/__init__.py +0 -0
  131. bot_cmder-0.2.0/tests/auth/test_acl.py +70 -0
  132. bot_cmder-0.2.0/tests/auth/test_emergency.py +179 -0
  133. bot_cmder-0.2.0/tests/auth/test_lockout.py +347 -0
  134. bot_cmder-0.2.0/tests/auth/test_pending.py +51 -0
  135. bot_cmder-0.2.0/tests/auth/test_secret_store.py +60 -0
  136. bot_cmder-0.2.0/tests/auth/test_totp.py +66 -0
  137. bot_cmder-0.2.0/tests/cli/__init__.py +0 -0
  138. bot_cmder-0.2.0/tests/cli/test_discord_register.py +140 -0
  139. bot_cmder-0.2.0/tests/cli/test_dispatcher.py +64 -0
  140. bot_cmder-0.2.0/tests/cli/test_init.py +149 -0
  141. bot_cmder-0.2.0/tests/cli/test_keys.py +31 -0
  142. bot_cmder-0.2.0/tests/cli/test_serve.py +130 -0
  143. bot_cmder-0.2.0/tests/cli/test_slack_manifest.py +182 -0
  144. bot_cmder-0.2.0/tests/commands/__init__.py +0 -0
  145. bot_cmder-0.2.0/tests/commands/test_health.py +121 -0
  146. bot_cmder-0.2.0/tests/commands/test_kubectl.py +97 -0
  147. bot_cmder-0.2.0/tests/commands/test_otp.py +436 -0
  148. bot_cmder-0.2.0/tests/commands/test_runbook.py +164 -0
  149. bot_cmder-0.2.0/tests/commands/test_service.py +388 -0
  150. bot_cmder-0.2.0/tests/commands/test_ssh.py +140 -0
  151. bot_cmder-0.2.0/tests/commands/test_whoami.py +46 -0
  152. bot_cmder-0.2.0/tests/config/__init__.py +0 -0
  153. bot_cmder-0.2.0/tests/config/test_paths.py +150 -0
  154. bot_cmder-0.2.0/tests/config/test_schema.py +81 -0
  155. bot_cmder-0.2.0/tests/config/test_settings_discovery.py +119 -0
  156. bot_cmder-0.2.0/tests/conftest.py +60 -0
  157. bot_cmder-0.2.0/tests/connectors/__init__.py +0 -0
  158. bot_cmder-0.2.0/tests/connectors/test_local.py +33 -0
  159. bot_cmder-0.2.0/tests/connectors/test_ssh.py +184 -0
  160. bot_cmder-0.2.0/tests/core/__init__.py +0 -0
  161. bot_cmder-0.2.0/tests/core/test_dispatcher.py +407 -0
  162. bot_cmder-0.2.0/tests/core/test_parser.py +76 -0
  163. bot_cmder-0.2.0/tests/core/test_redact.py +124 -0
  164. bot_cmder-0.2.0/tests/core/test_registry.py +50 -0
  165. bot_cmder-0.2.0/tests/core/test_router.py +128 -0
  166. bot_cmder-0.2.0/tests/storage/__init__.py +0 -0
  167. bot_cmder-0.2.0/tests/storage/test_migrator.py +136 -0
  168. bot_cmder-0.2.0/tests/test_cli.py +65 -0
  169. bot_cmder-0.2.0/tests/test_main.py +321 -0
  170. bot_cmder-0.2.0/uv.lock +1651 -0
@@ -0,0 +1,69 @@
1
+ # Keep the build context small + deterministic — only the files that
2
+ # the wheel build actually needs. Everything else is either:
3
+ # - re-derivable (.venv, dist, __pycache__)
4
+ # - state that doesn't belong in an image (var, .env)
5
+ # - irrelevant to the runtime (tests, docs, CI configs)
6
+ # - agent-local (.claude)
7
+
8
+ # Version control + IDE state
9
+ .git
10
+ .gitignore
11
+ .gitleaks.toml
12
+ .gitattributes
13
+ .editorconfig
14
+ .vscode
15
+ .idea
16
+
17
+ # Agent worktrees, plans, ephemeral state
18
+ .claude
19
+
20
+ # Python build / test artifacts
21
+ .venv
22
+ .pytest_cache
23
+ .ruff_cache
24
+ .mypy_cache
25
+ .coverage
26
+ htmlcov
27
+ **/__pycache__
28
+ **/*.pyc
29
+ **/*.pyo
30
+
31
+ # Local build outputs (wheels are rebuilt inside the builder stage)
32
+ dist
33
+ build
34
+ *.egg-info
35
+
36
+ # Repo-local bot state (never ship audit logs, totp secrets, etc.)
37
+ var
38
+ runbooks
39
+
40
+ # Operator config — must be mounted at runtime, never baked in
41
+ .env
42
+ .env.example
43
+ .env.local
44
+ config/app.yaml
45
+ config/app.yaml.local
46
+
47
+ # Tests + dev-only files don't run inside the image
48
+ tests
49
+ .pre-commit-config.yaml
50
+
51
+ # Docs, examples, scripts that don't ship in the wheel
52
+ docs
53
+ scripts/tunnel*.sh
54
+ scripts/show_env_settings.py
55
+ scripts/hook_status.py
56
+
57
+ # CI configuration — building from source doesn't need these inside
58
+ # the container, GitHub Actions reads them from the repo directly
59
+ .github
60
+
61
+ # OS / editor cruft
62
+ .DS_Store
63
+ Thumbs.db
64
+ *.swp
65
+ *.swo
66
+ *~
67
+
68
+ # Justfile — only needed for source contributors, not in the image
69
+ Justfile
@@ -0,0 +1,161 @@
1
+ # Telegram bot token from @BotFather.
2
+ TELEGRAM_TOKEN=
3
+
4
+ # Optional: any long random string. When set, the bot rejects webhook
5
+ # requests whose `X-Telegram-Bot-Api-Secret-Token` header does not match.
6
+ # Set the same value when calling setWebhook (see Justfile).
7
+ TELEGRAM_WEBHOOK_SECRET=
8
+
9
+ # Phase 6a — ingestion mode.
10
+ # - "webhook" (default): bot exposes POST /webhooks/telegram and
11
+ # Telegram POSTs each update there. Requires public HTTPS URL
12
+ # (set TELEGRAM_HOOK_URL below or use `just tunnel-ngrok`).
13
+ # - "polling": bot dials OUT to api.telegram.org and long-polls for
14
+ # updates. NO public URL, NO tunnel needed — ideal for home labs
15
+ # / NAT / corp egress restrictions. The two modes are mutually
16
+ # exclusive in Telegram's API; the daemon auto-deletes any
17
+ # registered webhook at startup so flipping the mode Just Works.
18
+ TELEGRAM_MODE=webhook
19
+
20
+ # Polling-mode tuning (ignored unless TELEGRAM_MODE=polling).
21
+ # Telegram caps the long-poll wait at 50s; 25s is a sweet spot —
22
+ # fewer requests than short polling, but stays under most middleboxes'
23
+ # silent-drop window.
24
+ TELEGRAM_POLLING_TIMEOUT_S=25
25
+ # When flipping to polling, drop any updates Telegram queued while a
26
+ # webhook was active. Useful during dev mode flapping (don't replay
27
+ # yesterday's tests). Leave false in prod so a brief webhook outage
28
+ # doesn't lose updates.
29
+ TELEGRAM_POLLING_DROP_PENDING=false
30
+
31
+ # Public HTTPS URL where Telegram will deliver updates. Used by
32
+ # `just set-telegram-bot-webhook` only — not read by the running app.
33
+ # Auto-overwritten by `just tunnel` and `just tunnel-ngrok`.
34
+ # Ignored entirely in TELEGRAM_MODE=polling.
35
+ TELEGRAM_HOOK_URL=
36
+
37
+ # Reserved ngrok static domain for `just tunnel-ngrok`. Get a free
38
+ # one at https://dashboard.ngrok.com/domains. Stable across ngrok
39
+ # restarts, so DNS never has propagation lag (unlike trycloudflare).
40
+ #
41
+ # IMPORTANT: just the bare hostname — no `https://` prefix, no path
42
+ # suffix. The tunnel script adds those itself; pasting the full URL
43
+ # corrupts TELEGRAM_HOOK_URL and ngrok rejects with ERR_NGROK_9038.
44
+ # ✓ NGROK_DOMAIN=your-reserved-domain.ngrok-free.dev
45
+ # ✗ NGROK_DOMAIN=https://your-reserved-domain.ngrok-free.dev/webhooks/telegram
46
+ NGROK_DOMAIN=
47
+
48
+ # Path to the app YAML config (users, ACL, healthcheck targets, ...).
49
+ APP_CONFIG_PATH=./config/app.yaml
50
+
51
+ # Optional override for the audit log path. Falls back to the path set
52
+ # in app.yaml under `audit.path`.
53
+ # AUDIT_PATH=./var/audit.jsonl
54
+
55
+ # Optional uvicorn bind overrides. Defaults: 127.0.0.1:47823 with no
56
+ # autoreload. Port 47823 is intentionally uncommon to avoid clashing
57
+ # with other dev services on 8000 / 8080.
58
+ # BIND_HOST=127.0.0.1
59
+ # BIND_PORT=47823
60
+ # RELOAD=1
61
+
62
+ # Phase 2 — Fernet symmetric key used to encrypt TOTP secrets at rest
63
+ # in the SQLite store. REQUIRED to enable the OTP gate; without it
64
+ # privileged commands (/kubectl, /runbook run, anything with
65
+ # Risk.PRIVILEGED) cannot be authorized and refuse to run.
66
+ #
67
+ # Generate one with:
68
+ # python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
69
+ #
70
+ # WARNING: rotating this key invalidates every existing TOTP enrollment.
71
+ # All users will need to re-run `python -m bot_cmder.cli enroll-totp`.
72
+ BOT_CMDER_MASTER_KEY=
73
+
74
+ # Phase 4 — Discord adapter. All three are required for the adapter
75
+ # to mount /webhooks/discord; without them the bot still runs but
76
+ # Discord is silently disabled (logged at WARNING during startup).
77
+ #
78
+ # DISCORD_PUBLIC_KEY: Ed25519 verify key (hex). Discord application
79
+ # "General Information" → "Public Key". Used to verify every
80
+ # incoming interaction; Discord refuses to onboard endpoints that
81
+ # don't enforce signing, so this is non-negotiable.
82
+ DISCORD_PUBLIC_KEY=
83
+ # DISCORD_BOT_TOKEN: bot token from "Bot" page. Used to PATCH
84
+ # deferred replies via Discord's webhook API and to PUT the slash
85
+ # command schema (scripts/register_discord_commands.py).
86
+ DISCORD_BOT_TOKEN=
87
+ # DISCORD_APPLICATION_ID: numeric application ID (in the URL of the
88
+ # application page). Goes into the follow-up @original PATCH path.
89
+ DISCORD_APPLICATION_ID=
90
+
91
+ # DISCORD_GUILD_ID: optional. Read ONLY by scripts/register_discord_commands.py
92
+ # — the running bot never touches this value. When set, `just register-discord`
93
+ # pushes the slash command schema to that one server (instant propagation,
94
+ # recommended for dev). When unset, registration goes global (~1h propagation,
95
+ # visible in every server the bot is in + DMs, recommended for prod).
96
+ # Override per-call with `just register-discord --guild=<id>` or force a
97
+ # global push with `just register-discord --global`.
98
+ #
99
+ # Get the ID: Discord client → Settings → Advanced → Developer Mode (on),
100
+ # then right-click your server icon → Copy Server ID.
101
+ DISCORD_GUILD_ID=
102
+
103
+ # Phase 6c — ingestion mode for the Discord adapter.
104
+ # - "interactions" (default): bot exposes POST /webhooks/discord and
105
+ # Discord POSTs slash commands there. Requires public HTTPS URL +
106
+ # DISCORD_PUBLIC_KEY for Ed25519 verification. This is Phase 4
107
+ # behavior, preserved as the default.
108
+ # - "gateway": bot opens a WebSocket OUT to Discord and chat events
109
+ # arrive over that. NO public URL needed; ideal for home labs / NAT.
110
+ # **Slash commands DON'T arrive over the Gateway** (Discord
111
+ # platform limitation) — UX shifts to `@bot cmd args` in guild
112
+ # channels or plain `cmd args` in DMs. Needs the MESSAGE_CONTENT
113
+ # privileged intent enabled in the dev portal (Bot → Privileged
114
+ # Gateway Intents → Message Content Intent).
115
+ # Anything other than "interactions" / "gateway" fails fast at startup.
116
+ DISCORD_MODE=interactions
117
+
118
+ # Phase 5/6b — Slack adapter. Two ingestion modes (mutually exclusive
119
+ # in Slack's app config — when Socket Mode is toggled on, the Events
120
+ # API URL field greys out):
121
+ # - "events" (default): /webhooks/slack receives Slack POSTs.
122
+ # Needs SLACK_SIGNING_SECRET. Requires public HTTPS URL.
123
+ # - "socket": bot opens WebSocket OUT to Slack. Needs SLACK_APP_TOKEN.
124
+ # NO public URL needed; ideal for home labs / NAT.
125
+ # Without the relevant secret(s) the bot still runs but Slack is
126
+ # silently disabled (logged at WARNING during startup).
127
+ SLACK_MODE=events
128
+
129
+ # SLACK_SIGNING_SECRET (events mode only): 32-char hex string from
130
+ # your Slack app's "Basic Information" → "App Credentials" →
131
+ # "Signing Secret". Used to verify every incoming slash command via
132
+ # HMAC-SHA256(`v0:<ts>:<body>`). Slack rejects endpoints that accept
133
+ # unsigned payloads.
134
+ SLACK_SIGNING_SECRET=
135
+
136
+ # SLACK_APP_TOKEN (socket mode only): app-level token from "Basic
137
+ # Information" → "App-Level Tokens" → Generate (scope:
138
+ # `connections:write`). Distinct from SLACK_BOT_TOKEN — this one
139
+ # authorizes opening the Socket Mode WebSocket connection.
140
+ SLACK_APP_TOKEN=
141
+
142
+ # SLACK_BOT_TOKEN: bot user OAuth token from "OAuth & Permissions"
143
+ # page (xoxb-...). Optional for both modes today; reserved for
144
+ # future chat.postMessage-based replies (Block Kit etc.).
145
+ SLACK_BOT_TOKEN=
146
+
147
+ # Read ONLY by scripts/register_slack_commands.py (the running bot
148
+ # never touches it — Slack tells the bot where the request came from
149
+ # per-call). Public HTTPS URL Slack should POST slash commands to.
150
+ # Bare hostname OK — the script appends /webhooks/slack:
151
+ # ✓ SLACK_REQUEST_URL=my-tunnel.ngrok-free.dev
152
+ # ✓ SLACK_REQUEST_URL=https://my-tunnel.ngrok-free.dev
153
+ # ✓ SLACK_REQUEST_URL=https://my-tunnel.ngrok-free.dev/webhooks/slack
154
+ # When unset, falls back to NGROK_DOMAIN. When that's also unset,
155
+ # `just register-slack` errors out so a placeholder URL never lands
156
+ # in the Slack manifest.
157
+ SLACK_REQUEST_URL=
158
+
159
+ # Reply visibility (ephemeral / in_channel / by_risk + per-command
160
+ # overrides) is configured in `config/app.yaml` under `slack:`, NOT
161
+ # here — it's tunable behavior, not a secret.
@@ -0,0 +1,41 @@
1
+ # Keeps deps and Action versions fresh by opening one PR per upgrade.
2
+ # Each PR runs through CI like any other change, so you only have to
3
+ # look at green/red instead of remembering when you last ran
4
+ # `uv sync --upgrade`.
5
+ #
6
+ # Cadence rationale:
7
+ # - "weekly" not "daily": daily floods the inbox; weekly batches
8
+ # enough churn to be worth reviewing without going stale.
9
+ # - Caps at 5 open PRs per ecosystem so a bad upstream week
10
+ # (e.g. 20 actions get patches at once) doesn't bury everything.
11
+
12
+ version: 2
13
+
14
+ updates:
15
+ # GitHub Actions used in .github/workflows/*.yml — pins like
16
+ # actions/checkout@v4 will get bumped to v5 etc.
17
+ - package-ecosystem: "github-actions"
18
+ directory: "/"
19
+ schedule:
20
+ interval: "weekly"
21
+ open-pull-requests-limit: 5
22
+ commit-message:
23
+ prefix: "ci"
24
+ include: "scope"
25
+
26
+ # Python deps tracked via pyproject.toml + uv.lock. Dependabot's
27
+ # `pip` ecosystem reads pyproject and updates uv.lock alongside it
28
+ # (it has uv support since late 2024).
29
+ - package-ecosystem: "pip"
30
+ directory: "/"
31
+ schedule:
32
+ interval: "weekly"
33
+ open-pull-requests-limit: 5
34
+ # Group all dev-only updates into ONE PR per week — they rarely
35
+ # need individual scrutiny (it's just lint/test tooling).
36
+ groups:
37
+ dev-tools:
38
+ dependency-type: "development"
39
+ commit-message:
40
+ prefix: "deps"
41
+ include: "scope"
@@ -0,0 +1,100 @@
1
+ # Test PyPI publish — issue #20 PR-B.
2
+ #
3
+ # Triggered by pushing to the `beta` branch. Mirrors the
4
+ # `zondatw/remote-cmder` workflow pattern (branch-based triggers
5
+ # instead of tag-based) so the publish flow feels familiar across
6
+ # Zonda's repos:
7
+ #
8
+ # git checkout beta
9
+ # git merge main # forward whatever was just verified
10
+ # git push # ← this is the trigger
11
+ #
12
+ # After the workflow goes green, eyeball https://test.pypi.org/project/bot-cmder/
13
+ # in a browser. If the page renders the README correctly, license
14
+ # shows MIT, and `pip install --index-url https://test.pypi.org/simple/
15
+ # --extra-index-url https://pypi.org/simple/ bot-cmder` works in a
16
+ # fresh venv → safe to forward to the `release` branch.
17
+ #
18
+ # Auth: PyPI Trusted Publishing (OIDC). No long-lived `TWINE_PASSWORD`
19
+ # in repo secrets — GitHub mints a short-lived token at publish time
20
+ # and PyPI validates it against the pending publisher configured on
21
+ # the test.pypi.org side. See docs/release.md for the one-time setup.
22
+ #
23
+ # Why a separate `beta` branch and not a `--repository test.pypi.org`
24
+ # flag on a workflow_dispatch: matches the repo-mate pattern, and a
25
+ # dedicated branch's HEAD makes "what's currently on test.pypi.org?"
26
+ # answerable with `git log beta`.
27
+
28
+ name: beta
29
+
30
+ on:
31
+ push:
32
+ branches: [beta]
33
+
34
+ # Don't cancel an in-flight publish if a second push lands on beta —
35
+ # that would leave test.pypi.org in an inconsistent state. Each push
36
+ # waits for the previous to finish.
37
+ concurrency:
38
+ group: beta-publish
39
+ cancel-in-progress: false
40
+
41
+ jobs:
42
+ build:
43
+ name: build wheel + sdist
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@v6
47
+
48
+ - uses: astral-sh/setup-uv@v7
49
+ with:
50
+ enable-cache: true
51
+
52
+ - name: Install Python
53
+ run: uv python install 3.12
54
+
55
+ - name: Build distributions
56
+ run: uv build
57
+
58
+ - name: Verify wheel ships expected data files
59
+ run: |
60
+ set -euo pipefail
61
+ WHEEL=$(ls dist/*.whl)
62
+ echo "Inspecting $WHEEL"
63
+ unzip -l "$WHEEL" | grep -q 'bot_cmder/data/app.yaml.example' || (
64
+ echo "ERROR: bot_cmder/data/app.yaml.example missing from wheel"
65
+ exit 1
66
+ )
67
+ unzip -l "$WHEEL" | grep -q 'bot_cmder/auth/migrations/0001_initial.sql' || (
68
+ echo "ERROR: bot_cmder/auth/migrations/0001_initial.sql missing from wheel"
69
+ exit 1
70
+ )
71
+ echo "OK — wheel contents verified"
72
+
73
+ - uses: actions/upload-artifact@v7
74
+ with:
75
+ name: dist
76
+ path: dist/
77
+ retention-days: 7
78
+
79
+ publish-test:
80
+ name: publish to test.pypi.org
81
+ needs: build
82
+ runs-on: ubuntu-latest
83
+ environment:
84
+ name: release-test
85
+ url: https://test.pypi.org/project/bot-cmder/
86
+ permissions:
87
+ # Required for PyPI Trusted Publishing — GitHub mints a short-
88
+ # lived OIDC token that test.pypi.org exchanges for an upload
89
+ # credential. No long-lived API token in repo secrets.
90
+ id-token: write
91
+ steps:
92
+ - uses: actions/download-artifact@v8
93
+ with:
94
+ name: dist
95
+ path: dist/
96
+
97
+ - name: Publish to Test PyPI
98
+ uses: pypa/gh-action-pypi-publish@release/v1
99
+ with:
100
+ repository-url: https://test.pypi.org/legacy/
@@ -0,0 +1,200 @@
1
+ # GitHub Actions CI — runs the full test suite + lint on every push to
2
+ # main and on every PR. Mirrors what `just test` and `pre-commit run
3
+ # --all-files` do locally so a green CI badge actually means something.
4
+ #
5
+ # Why two jobs (test + lint) instead of one:
6
+ # - Different inputs change them independently — a doc typo touches
7
+ # lint only, a handler change touches test only. Splitting lets
8
+ # you see at a glance which side broke.
9
+ # - They can run in parallel (faster red-line on broken PRs).
10
+ #
11
+ # Why matrix Python 3.10 and 3.12 (skipping 3.11):
12
+ # - 3.10 is the floor declared in pyproject.toml `requires-python`
13
+ # and `tool.ruff.target-version`. If we don't test it, "supports
14
+ # 3.10" is a lie.
15
+ # - 3.12 is the latest widely-deployed stable. Catches accidental
16
+ # use of removed-in-3.12 APIs (e.g. distutils, asyncio.coroutine).
17
+ # - Skipping 3.11 halves matrix runtime; if either neighbor passes,
18
+ # 3.11 almost always does too.
19
+
20
+ name: CI
21
+
22
+ on:
23
+ push:
24
+ branches: [main]
25
+ pull_request:
26
+
27
+ # Cancel a previous in-flight run for the same ref when a new commit
28
+ # lands on a PR — saves CI minutes and gets the latest result faster.
29
+ # Pushes to main are NOT cancelled (the `|| github.run_id` makes each
30
+ # main push its own group).
31
+ concurrency:
32
+ group: ci-${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'push' && github.run_id || '' }}
33
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
34
+
35
+ jobs:
36
+ test:
37
+ name: pytest (py${{ matrix.python-version }})
38
+ runs-on: ubuntu-latest
39
+ strategy:
40
+ fail-fast: false
41
+ matrix:
42
+ python-version: ["3.10", "3.12"]
43
+ steps:
44
+ - name: Check out repo
45
+ uses: actions/checkout@v6
46
+
47
+ - name: Install uv (with cache)
48
+ uses: astral-sh/setup-uv@v7
49
+ with:
50
+ enable-cache: true
51
+ cache-dependency-glob: "uv.lock"
52
+
53
+ - name: Pin Python ${{ matrix.python-version }}
54
+ run: uv python install ${{ matrix.python-version }}
55
+
56
+ - name: Sync dependencies (locked)
57
+ # --frozen: refuse to update uv.lock; CI must use the exact
58
+ # versions a developer committed, otherwise "works on my
59
+ # machine" creeps back in via silent upgrades.
60
+ run: uv sync --frozen --python ${{ matrix.python-version }}
61
+
62
+ - name: Run pytest
63
+ run: uv run --python ${{ matrix.python-version }} pytest
64
+
65
+ lint:
66
+ name: pre-commit
67
+ runs-on: ubuntu-latest
68
+ steps:
69
+ - name: Check out repo
70
+ uses: actions/checkout@v6
71
+
72
+ - name: Set up Python 3.12
73
+ # Single Python is enough — black / ruff / codespell are all
74
+ # version-agnostic for our codebase.
75
+ uses: actions/setup-python@v6
76
+ with:
77
+ python-version: "3.12"
78
+
79
+ - name: Run pre-commit hooks
80
+ # The official action handles caching of hook envs (which
81
+ # otherwise re-clone black + ruff repos on every run).
82
+ uses: pre-commit/action@v3.0.1
83
+
84
+ # Issue #20 — wheel-build smoke test runs the same build steps the
85
+ # release workflows (.github/workflows/{beta,release}.yml) do, but
86
+ # on every PR. Catches packaging regressions at PR time instead of
87
+ # at release time. Examples of what would have shipped broken
88
+ # without this gate:
89
+ # - someone deletes bot_cmder/data/__init__.py → app.yaml.example
90
+ # stops shipping in the wheel → `bot-cmder init` 500s on every
91
+ # install
92
+ # - a refactor breaks the [project.scripts] entry → `bot-cmder`
93
+ # console script absent after `pip install`
94
+ # - import error in cli/__init__.py → `bot-cmder --help` raises
95
+ # before printing usage
96
+ # Each of those is silent in the test suite (which uses the editable
97
+ # install) but breaks the wheel.
98
+ wheel-build:
99
+ name: wheel build smoke test
100
+ runs-on: ubuntu-latest
101
+ steps:
102
+ - name: Check out repo
103
+ uses: actions/checkout@v6
104
+
105
+ - name: Install uv (with cache)
106
+ uses: astral-sh/setup-uv@v7
107
+ with:
108
+ enable-cache: true
109
+ cache-dependency-glob: "uv.lock"
110
+
111
+ - name: Pin Python 3.12
112
+ run: uv python install 3.12
113
+
114
+ - name: Build wheel + sdist
115
+ run: uv build
116
+
117
+ - name: Verify wheel ships expected data files
118
+ # Same checks the release workflows do — keep in sync. The
119
+ # set is small enough to inline; if it grows, factor into a
120
+ # shared bash script under scripts/.
121
+ run: |
122
+ set -euo pipefail
123
+ WHEEL=$(ls dist/*.whl)
124
+ echo "Inspecting $WHEEL"
125
+ unzip -l "$WHEEL" | grep -q 'bot_cmder/data/app.yaml.example' || (
126
+ echo "ERROR: bot_cmder/data/app.yaml.example missing from wheel"
127
+ exit 1
128
+ )
129
+ unzip -l "$WHEEL" | grep -q 'bot_cmder/auth/migrations/0001_initial.sql' || (
130
+ echo "ERROR: bot_cmder/auth/migrations/0001_initial.sql missing from wheel"
131
+ exit 1
132
+ )
133
+ echo "OK — wheel data files present"
134
+
135
+ - name: Smoke install + run --version
136
+ # Fresh venv install of the wheel exercises:
137
+ # - [project.scripts] entry point lands `bot-cmder` on PATH
138
+ # - lazy imports in cli/__init__.py + subcommands resolve
139
+ # - bot_cmder.__version__ is accessible at runtime
140
+ # If `bot-cmder --version` exits non-zero or prints empty, fail.
141
+ run: |
142
+ set -euo pipefail
143
+ python3 -m venv /tmp/smoke
144
+ /tmp/smoke/bin/pip install --quiet dist/*.whl
145
+ ver=$(/tmp/smoke/bin/bot-cmder --version)
146
+ echo "Got: $ver"
147
+ echo "$ver" | grep -qE '^bot-cmder [0-9]+\.[0-9]+\.[0-9]+' || (
148
+ echo "ERROR: --version output didn't match 'bot-cmder X.Y.Z'"
149
+ exit 1
150
+ )
151
+ # Also exercise the no-args case to catch a regression in
152
+ # argparse setup (e.g. missing required=True on subparsers
153
+ # would silently no-op instead of printing usage + exiting 2).
154
+ /tmp/smoke/bin/bot-cmder >/dev/null 2>&1 && (
155
+ echo "ERROR: bot-cmder with no args should exit non-zero"
156
+ exit 1
157
+ ) || echo "OK — no-args path exits non-zero as expected"
158
+
159
+ # Phase 7 — secret-scan is its own job (not a step in lint) because
160
+ # it's a security gate, not a style gate. When it fails on a PR the
161
+ # status check name "secret-scan" is unambiguous about what broke,
162
+ # and the red badge is visually distinct from "pre-commit failed
163
+ # because black wanted to reformat". Runs in parallel with lint
164
+ # and test so it doesn't add to total CI wall-clock time.
165
+ secret-scan:
166
+ name: secret-scan
167
+ runs-on: ubuntu-latest
168
+ # gitleaks-action calls /repos/.../pulls/N/commits to enumerate
169
+ # the PR's commits. The default GITHUB_TOKEN doesn't grant that
170
+ # scope (`Resource not accessible by integration` 403 otherwise).
171
+ # Read-only `pull-requests: read` is the minimum that satisfies
172
+ # the API call without giving the action write access to anything.
173
+ permissions:
174
+ contents: read
175
+ pull-requests: read
176
+ steps:
177
+ - name: Check out repo
178
+ uses: actions/checkout@v6
179
+ with:
180
+ # gitleaks needs the FULL history to scan every commit in
181
+ # the PR (not just the merge tip). Default checkout is a
182
+ # shallow clone with depth=1; override to fetch everything.
183
+ fetch-depth: 0
184
+
185
+ - name: Run gitleaks
186
+ # Pinned action version + the same gitleaks binary version
187
+ # we run locally (.pre-commit-config.yaml). Config is
188
+ # auto-discovered from .gitleaks.toml at the repo root.
189
+ uses: gitleaks/gitleaks-action@v2
190
+ env:
191
+ # Required by gitleaks-action@v2 for scanning pull
192
+ # requests (announced as a breaking change in their
193
+ # README). The auto-provisioned per-run token is
194
+ # sufficient — no PAT needed.
195
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
196
+ # Quiet the marketing pop-up about gitleaks-pro that the
197
+ # action would otherwise print on every run for OSS repos.
198
+ GITLEAKS_NOTIFY_USER_LIST: ""
199
+ GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "false"
200
+ GITLEAKS_ENABLE_SUMMARY: "true"