v3.0.0 #1

Merged
zevaryx merged 46 commits from dev into main 2026-11-03 23:28:46 +00:00
Owner

Companion release for the JARVIS 3.0.0 release

Companion release for the JARVIS 3.0.0 release
Neither module was imported, their dependencies were already commented out, and both referenced config.reddit/config.twitter, which do not exist on Config, so they could not even be imported. Also drop their (and Mastodon's) unused settings from sample.env.

BREAKING CHANGE: the reddit and twitter sync tasks are gone.
Move metadata to the PEP 621 [project] table and dev tools to a PEP 735 dependency group, replace poetry.lock with uv.lock, and build the Docker image with uv. The project is not packaged (run.py imports it in place), so uv only installs its dependencies.

jarvis-core now comes from its current home, git.zevaryx.com/jarvis/jarvis-core (the old stark-industries path no longer exists), pinned to the same commit the Poetry lock used. Other constraints are unchanged.
The reminder task imports pytz but it was never declared; it only arrived transitively through older croniter releases, and croniter 6 no longer depends on it.
StatTracker was never started, so nothing ever exposed the metrics, and most of them belonged to the removed reddit/twitter tasks. Drop the module, the reminder counter call (which also crashed for reminders without a guild channel), and the uvicorn and prometheus-client dependencies.
jarvis-core 2.0 (Beanie 2 on PyMongo's async client, IDs stored as int64), interactions-py 5.16, croniter 6, pydantic 2.14, pydantic-settings 2.16 and aiohttp 3.14; pytest 9 and black 26 for development. Beanie is now declared directly since the tasks import beanie.operators.

BREAKING CHANGE: requires a database migrated with jarvis-core 2.0's jarvis-migrate.
Mongo was a BaseSettings without an env prefix, instantiated at import time, so it read the unprefixed HOST/USERNAME/PASSWORD/PORT variables (on Windows, USERNAME is the OS login name) while the MONGO_* names documented in sample.env were ignored. Loading sample.env also failed outright because keys meant for other services were rejected as extra inputs.

Mongo now uses the MONGO_ prefix and a per-instance default, extra keys are ignored, empty values fall back to defaults, and MONGO_DATABASE/MONGO_AUTH_SOURCE are passed through to connect(). sample.env lists only what the tasks read, and the config is covered by tests.
Jobs were started with a bare asyncio.create_task(runat(...)), which only holds a weak reference, so a sleeping job could be garbage-collected. A job that raised also skipped its queue.remove(), leaving that document stuck as "already scheduled" until restart, and the exception was lost as "Task exception was never retrieved".

Replace runat() with schedule(), which keeps a strong reference, logs failures, and removes the key from the pending set in a finally block. Queues are now sets.
None of the task loops caught exceptions, so a single error (a guild the bot left, a deleted channel, a bad document) ended that task for the life of the process. The entry point also awaited the tasks one at a time, so a dead task was never noticed while an earlier one was still running, and its KeyboardInterrupt handler could reference an unbound name.

Each loop body is now a _poll() run by run_forever(), which logs a failed pass and tries again after the usual interval, and the entry point gathers all loops.
The poll compared Ban.duration (hours, an int) to a datetime, which MongoDB never matches, so temp bans never expired; and had one matched, _unban() was called with five arguments for four parameters. Select bans by the new Ban.expires_at instead, pass the right arguments, and record the Unban from the stored ban when the guild or user can no longer be fetched.
The job looked up the guild and channel at schedule time: a guild the bot had left returned None and crashed the loop on guild.fetch_channel, and Guild.fetch_channel raises NotFound for a deleted channel. Fetch the channel when the job fires (Client.fetch_channel returns None for deleted channels), fall back to the channel's guild for locks saved before Lock.guild existed, log permission errors, and always close the lock.
As with locks, the guild and @everyone role were fetched at schedule time and a guild the bot had left crashed the loop. Resolve them when the lift fires, log permission errors, and always close the lockdown.
Jobs acted on the copy loaded when they were queued, up to an hour earlier, so a manual /unlock, /unban or /lockdown end did not cancel them: the lock job re-applied stale permissions, and an expired temp ban unbanned a user who had since been banned again. Each job now reloads its document and skips it if it was lifted, removed or rescheduled; an expired temp ban is closed without unbanning while the user has another active ban.
When the DM failed, the fallback assumed a guild text channel and crashed for reminders set in DMs (DMChannel has no guild) or whose channel was deleted (None). A reminder that could not be delivered anywhere was deleted outright, so it was lost.

The fallback now only uses guild channels, undelivered reminders are disabled and kept so /reminders fetch can still show them (the notice also named a nonexistent /reminder fetch command), repeating reminders with an unknown timezone fall back to UTC, and the embed no longer appends the legacy #0 discriminator.
The comparison was inverted, so it would have kicked members who joined recently rather than overdue ones; it read role.members from a gateway cache the REST-only tasks client never fills; the member pages it fetched were discarded and the resync timestamps never updated; the kick reason was not an f-string; and a non-numeric setting crashed the loop.

Members are now listed over the REST API and those still holding the unverified role more than N days after joining are kicked. Invalid settings are removed.
build_embed() defaulted to a naive datetime.now(), which Discord renders as if it were UTC, so embeds showed the server's local time with the wrong offset.
start() and _start() each loaded the config, start() declared an unused global, the docstrings described a config path that is not taken, and the login message appended the legacy #0 discriminator. Pass the loaded config through instead, and let interactions.py manage its own event loop.
The image ran the service as root. It only needs to read /app, so run it as a dedicated system user.
pre-commit-hooks 6.0.0, black 26.10.1, isort 9.0.2 and flake8 7.5.0, and pre-commit 4.7.0 itself. isort moves from the archived mirrors-isort repo to PyCQA/isort.
const.__version__ reads the installed package metadata, but the project was not installed as a package, so it always reported 0.0.0. Build it with uv_build, install it into the environment, and install it non-editable in the Docker image (after a cached dependency-only layer).
Creating a time-series collection adds system.views, which ordinary users cannot drop, so wiping the test database failed once one existed.
Replace the GitLab pipeline with a Forgejo Actions workflow: pre-commit, the test suite against a Mongo service, and a Docker build and push to both registries on main, dev and tags. Secrets used: ZEVARYX_REGISTRY_URL/USERNAME/PASSWORD and DOCKERHUB_REGISTRY_USERNAME/PASSWORD.
build: pin the latest jarvis-core
Some checks failed
CI / precommit (push) Failing after 1s
CI / test (push) Failing after 0s
CI / image (push) Has been skipped
502f0f3f2f
Move the jarvis-core pin from the 2.0.0 release commit to the current rewrite head, matching jarvis-bot. Relock.
ci: run workflows on ubuntu-latest
Some checks failed
CI / precommit (push) Failing after 2s
CI / test (push) Failing after 2s
CI / image (push) Has been skipped
bce7ddc7d6
Use the ubuntu-latest runner label instead of docker.
ci: run jobs in the runner image instead of python containers
Some checks failed
CI / test (push) Successful in 22s
CI / precommit (push) Failing after 45s
CI / image (push) Has been skipped
92e065de28
The python job containers have no Node, which actions/checkout needs. Drop them, install uv in the ubuntu-latest image and let it fetch Python, and build Docker images on ubuntu-full-latest.
style: format with black 26 and fix the flake8 findings
Some checks failed
CI / precommit (push) Successful in 44s
CI / test (push) Failing after 2m54s
CI / image (push) Has been skipped
f05899e026
CI runs pre-commit over the whole tree. Apply black 26's formatting and isort, and add the missing docstring to run.py.
The task tests waited 50 to 200 milliseconds for background jobs, which is too short when MongoDB round trips are slower, as on CI, and they passed or failed depending on timing. Wait on the jobs themselves (or poll for the condition) with a ten second ceiling, so they finish as soon as the work does and cannot race.
Add one extra matrix entry (Python 3.12) that installs interactions-py 6.0.0rc1 over the locked version before running the tests. Locally, jarvis-bot's 214 tests and jarvis-tasks' 28 pass on it. The tests run with --no-sync so uv does not put the locked version back.
ci: build images on ubuntu-latest instead of the 16GB full image
Some checks failed
CI / test (6.0.0rc1, 3.12) (push) Successful in 44s
CI / precommit (push) Successful in 1m23s
CI / test (locked, 3.12) (push) Successful in 43s
CI / test (locked, 3.14) (push) Successful in 46s
CI / test (locked, 3.15) (push) Successful in 35s
CI / test (locked, 3.13) (push) Failing after 3m19s
CI / image (push) Has been skipped
01d8a16459
The slim act-latest runner image already ships the Docker CLI and buildx plugin, so the full image is not needed to build and push.
On a runner host whose containers default to a 1024 open-file limit, mongod aborts with WiredTiger 'Too many open files' partway through the suite and every later test fails with connection errors. Start the service with --ulimit nofile=64000:64000, the limit MongoDB recommends.
ci: run every test job against interactions-py 6.0.0rc1
Some checks failed
CI / test (6.0.0rc1, 3.12) (push) Successful in 22s
CI / precommit (push) Successful in 46s
CI / test (6.0.0rc1, 3.13) (push) Successful in 27s
CI / test (6.0.0rc1, 3.14) (push) Successful in 29s
CI / test (6.0.0rc1, 3.15) (push) Successful in 26s
CI / image (push) Failing after 6s
2590e4ad3d
Replace the single rc1 matrix entry with rc1 for all Python versions, so the tests no longer run against the locked 5.x release. Locally both suites pass on rc1.
The image job built an invalid reference when the registry secret was saved with a scheme, trailing slash or whitespace. Normalise the host, check it before building, and pass secrets through the environment instead of expanding them into the script. Tags are now: dev branch -> :dev, main -> :latest, git tags -> the tag, so people on :latest no longer get development builds. The old :stable tag is no longer published.
Make the 6.0 release candidate the real dependency instead of overriding it in CI: allow >=6.0.0rc1,<7 and relock. The 28 tests pass on it.
ci: drop the interactions-py override now that the lock uses 6.0.0rc1
All checks were successful
CI / test (3.12) (push) Successful in 24s
CI / precommit (push) Successful in 48s
CI / test (3.13) (push) Successful in 25s
CI / test (3.14) (push) Successful in 24s
CI / test (3.15) (push) Successful in 28s
CI / image (push) Successful in 37s
CI / test (3.12) (pull_request) Successful in 24s
CI / precommit (pull_request) Successful in 45s
CI / test (3.13) (pull_request) Successful in 27s
CI / test (3.14) (pull_request) Successful in 23s
CI / test (3.15) (pull_request) Successful in 26s
CI / image (pull_request) Has been skipped
9b50b17ace
The test jobs install the locked dependencies as-is, so the matrix no longer needs an interactions version or a pip override step.
Attach Snooze 10 minutes, 1 hour and 1 day buttons to a reminder delivered by DM or in its channel. jarvis-bot handles the press (custom id snooze|N). The notice sent when a private reminder cannot be delivered has no buttons, since there is nothing to snooze.
Move jarvis-tasks from 0.12.0 to 3.0.0 so it shares a version with jarvis-bot and jarvis-core, require jarvis-core >=3.0.0,<4 and pin the 3.0.0 commit, and say in the README that 3.x of each work together.
style: format the snooze button code
All checks were successful
CI / test (3.12) (push) Successful in 22s
CI / precommit (push) Successful in 44s
CI / test (3.13) (push) Successful in 24s
CI / test (3.14) (push) Successful in 29s
CI / test (3.15) (push) Successful in 26s
CI / test (3.12) (pull_request) Successful in 22s
CI / precommit (pull_request) Successful in 46s
CI / test (3.13) (pull_request) Successful in 26s
CI / test (3.14) (pull_request) Successful in 30s
CI / test (3.15) (pull_request) Successful in 26s
CI / image (pull_request) Has been skipped
CI / image (push) Successful in 29s
932bc6ddc2
Black formatting for the code added with the snooze buttons, so pre-commit passes in CI.
docs: describe the website, its cookies and sessions in the privacy policy and terms
All checks were successful
CI / test (3.12) (push) Successful in 23s
CI / precommit (push) Successful in 43s
CI / test (3.13) (push) Successful in 26s
CI / test (3.14) (push) Successful in 29s
CI / test (3.15) (push) Successful in 27s
CI / test (3.12) (pull_request) Successful in 24s
CI / precommit (pull_request) Successful in 43s
CI / test (3.13) (pull_request) Successful in 27s
CI / test (3.14) (pull_request) Successful in 30s
CI / test (3.15) (pull_request) Successful in 26s
CI / image (pull_request) Has been skipped
CI / image (push) Successful in 24s
6234d951d6
docs: describe ban appeals in the privacy policy
All checks were successful
CI / precommit (push) Successful in 48s
CI / test (3.12) (push) Successful in 27s
CI / test (3.13) (push) Successful in 26s
CI / test (3.14) (push) Successful in 30s
CI / test (3.15) (push) Successful in 26s
CI / test (3.12) (pull_request) Successful in 23s
CI / precommit (pull_request) Successful in 45s
CI / test (3.13) (pull_request) Successful in 27s
CI / test (3.14) (pull_request) Successful in 29s
CI / image (push) Successful in 11s
CI / test (3.15) (pull_request) Successful in 27s
CI / image (pull_request) Has been skipped
9b51adc37b
docs: describe retention and appeal notices in the privacy policy
All checks were successful
CI / test (3.12) (pull_request) Successful in 25s
CI / test (3.13) (pull_request) Successful in 28s
CI / precommit (pull_request) Successful in 1m25s
CI / test (3.14) (pull_request) Successful in 31s
CI / test (3.15) (pull_request) Successful in 26s
CI / image (pull_request) Has been skipped
CI / precommit (push) Successful in 38s
CI / test (3.12) (push) Successful in 22s
CI / test (3.13) (push) Successful in 24s
CI / test (3.14) (push) Successful in 25s
CI / test (3.15) (push) Successful in 24s
CI / image (push) Successful in 25s
52593b5012
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jarvis/jarvis-tasks!1
No description provided.