Skip to content

feat: image-based docker self-hosting - #225

Draft
Zingzy wants to merge 4 commits into
mainfrom
feat/docker-selfhost
Draft

feat: image-based docker self-hosting#225
Zingzy wants to merge 4 commits into
mainfrom
feat/docker-selfhost

Conversation

@Zingzy

@Zingzy Zingzy commented Jul 4, 2026

Copy link
Copy Markdown
Member

Self-hosting moves from "clone the repo and build" to the pattern every established OSS project uses (researched against LiteLLM, Shlink, Umami, Immich, n8n, Uptime Kuma, Plausible, Vaultwarden): wget one compose file, docker compose up -d, done.

The artifact

selfhost/docker-compose.yml — runs ghcr.io/spoo-me/spoo:${SPOO_VERSION:-latest} with bundled MongoDB + Redis (not host-exposed). Zero-edit boot; every knob is ${VAR:-default} so a .env beside the file or shell env overrides without editing it. The deploy workflow attaches it to every release, so releases/latest/download/docker-compose.yml always matches the newest image.

Fixes required to make zero-config honest

  • Tenant middleware sitewide-404 on unknown hosts — with custom domains disabled (the self-host default), any host the app wasn't told about (domain, IP, internal DNS, reverse-proxy name) returned 404 for everything including /health. Now passes through to the full app surface; SaaS mode (CUSTOM_DOMAINS_ENABLED=true, i.e. prod) keeps the strict 404, and already-registered domains keep resolving in both modes.
  • Empty JWT config crashed auth — pyjwt refuses empty HMAC keys, so zero-config register/login 500'd (InvalidKeyError). Boot now autogenerates an ephemeral HS256 secret with a loud jwt_secret_autogenerated warning; sessions don't survive restarts until JWT_SECRET is set.
  • .env.example placeholder JWT keys — the non-empty -----BEGIN PRIVATE KEY-----\n..... placeholders forced RS256 with garbage keys; first login crashed for anyone who kept them. Now empty with a comment explaining the HS256 fallback.

Verified e2e

Booted the compose zero-edit with the built image: health ✓, shorten ✓, register returns a signed token ✓, /health via a fake internal-domain Host header serves 200 ✓ (was sitewide 404). 1811 tests green.

⚠️ Launch blocker (org UI, no API)

ghcr.io/spoo-me/spoo is private — anonymous pulls fail (verified: token grant 401 vs Immich's 200). Nothing in this PR works for the public until: org Packages → spoo → Package settings → Change visibility → Public.

Docs rewrite (docker-deployment.mdx, PaaS repairs) is the follow-up.

Summary by Sourcery

Introduce an image-based Docker self-hosting flow with a bundled compose file while ensuring zero-config deployments behave correctly for JWT auth and tenant routing.

New Features:

  • Provide a self-hosting docker-compose.yml that runs the published application image alongside MongoDB and Redis with sensible defaults and env-based overrides.

Bug Fixes:

  • Ensure unknown hosts are served by the full app when custom domains are disabled, avoiding sitewide 404s in self-host mode.
  • Automatically generate a usable HS256 JWT secret when no keys are configured so zero-config auth no longer crashes on login.

Enhancements:

  • Refine JWT configuration handling by warning on weak secrets and centralizing secret bootstrap logic.
  • Adjust tenant middleware to make the unknown-host behavior configurable via a custom_domains_enabled flag while preserving existing custom-domain resolution.

Build:

  • Extend the deploy GitHub workflow to attach the self-host docker-compose.yml to release assets so the latest compose file is always available.

Tests:

  • Add unit tests covering tenant middleware behavior with custom domains disabled and JWT secret auto-generation for zero-config deployments.

Summary by CodeRabbit

  • New Features

    • Added a complete Docker Compose setup for self-hosting, including application, MongoDB, Redis, health checks, and persistent storage.
    • Self-hosted container images now support both AMD64 and ARM64 platforms.
    • Added automatic JWT secret generation when no signing secret is configured.
  • Bug Fixes

    • Custom-domain handling now allows standard requests through when custom domains are disabled.
    • Updated deployment releases to include the matching self-hosting configuration.
  • Security

    • Runtime containers now run as a non-root user.

Zingzy added 2 commits July 5, 2026 01:55
Three gaps between the everything-optional policy and reality:
- Unknown Host headers got a sitewide 404 even with custom domains
  disabled — any deploy behind a domain/IP the app wasn't told about was
  dead, including /health. Middleware now passes unknown hosts through
  to the full app when the feature is off (the SaaS 404 stays when on).
- Empty JWT config crashed register/login with InvalidKeyError (pyjwt
  refuses empty HMAC keys). Boot now autogenerates an ephemeral secret
  with a loud warning instead.
- .env.example shipped non-empty JWT placeholder keys, forcing RS256
  with garbage — auth crashed on first login for anyone who kept them.
selfhost/docker-compose.yml runs the published GHCR image with bundled
MongoDB and Redis — zero-edit boot, no clone, no build, no .env
required. Every setting is a ${VAR:-default} so a .env next to the file
or shell env overrides without editing. Version pinning via
SPOO_VERSION (Immich-style). The deploy workflow uploads the file to
each release, so releases/latest/download/docker-compose.yml always
matches the newest image.
Copilot AI review requested due to automatic review settings July 4, 2026 20:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds self-hosting Compose deployment assets, multi-architecture image publishing, a multi-stage non-root container build, automatic HS256 secret bootstrap, and configurable tenant pass-through behavior.

Changes

Self-hosted runtime

Layer / File(s) Summary
JWT secret bootstrap
.env.example, app.py, tests/unit/test_jwt_secret_bootstrap.py
JWT startup now generates an ephemeral HS256 secret when no RSA keys or secret are configured, preserves configured secrets, warns on weak secrets, and validates these behaviors.
Custom-domain routing mode
middleware/tenant.py, app.py, tests/unit/middleware/test_tenant.py
Tenant middleware receives the custom-domain feature flag and passes unknown hosts through when custom domains are disabled, while known tenant resolution remains covered by tests.
Multi-stage container build
dockerfile
Production dependencies are built in a separate stage, then copied into a non-root runtime image with APP_VERSION metadata and prebuilt bytecode.
Self-host service orchestration
selfhost/docker-compose.yml
Compose defines the app, MongoDB, and Redis services with environment overrides, healthchecks, health-gated startup, and persistent MongoDB volumes.
Release asset and multi-architecture publishing
.github/workflows/deploy.yml
Releases upload the Compose file, and Docker builds target both AMD64 and ARM64 using QEMU.
Estimated code review effort: 3 (Moderate) ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReleaseWorkflow
  participant ContainerRegistry
  participant DockerCompose
  participant app
  participant mongo
  participant redis
  ReleaseWorkflow->>ContainerRegistry: build and publish AMD64/ARM64 image
  ReleaseWorkflow->>DockerCompose: upload docker-compose.yml release asset
  DockerCompose->>mongo: start and check health
  DockerCompose->>redis: start and check health
  DockerCompose->>app: start after dependencies are healthy
Loading

Possibly related PRs

  • spoo-me/spoo#188: Also changes TenantMiddleware initialization and tenant-scoped request handling.

Suggested labels: backend

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding an image-based Docker self-hosting flow.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/docker-selfhost

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a zero-edit Docker Compose self-hosting flow backed by a published image, while fixing JWT bootstrap issues and relaxing tenant middleware behavior for non-SaaS deployments.

Sequence diagram for TenantMiddleware behavior with custom domains disabled

sequenceDiagram
  actor Client
  participant FastAPI
  participant TenantMiddleware
  participant AppRoute

  Client->>FastAPI: HTTP request (Host header)
  FastAPI->>TenantMiddleware: dispatch(request, call_next)
  TenantMiddleware->>TenantMiddleware: [tenant lookup]

  alt tenant found
    TenantMiddleware->>FastAPI: call_next(request)
    FastAPI->>AppRoute: route handler
    AppRoute-->>Client: response
  else tenant is None and custom_domains_enabled = false
    TenantMiddleware->>FastAPI: call_next(request)
    FastAPI->>AppRoute: route handler
    AppRoute-->>Client: full app response (e.g. /health 200)
  else tenant is None and custom_domains_enabled = true
    TenantMiddleware-->>Client: _tenant_not_found(request, tenant=None) (404)
  end
Loading

File-Level Changes

Change Details Files
Add a first-class self-host Docker Compose artifact and attach it automatically to GitHub releases.
  • Create selfhost/docker-compose.yml defining app, MongoDB, and Redis services with environment-driven configuration defaults.
  • Configure health checks for app, MongoDB, and Redis to ensure robust startup ordering.
  • Update the deploy GitHub Actions workflow to upload the compose file as a release asset on release events.
selfhost/docker-compose.yml
.github/workflows/deploy.yml
Harden and bootstrap JWT configuration to support zero-config deployments without crashing authentication.
  • Introduce _ensure_jwt_secret helper to generate an ephemeral HS256 secret when RS256 keys are absent and JWT_SECRET is empty, and warn on weak secrets.
  • Invoke _ensure_jwt_secret during app lifespan setup instead of the previous warning-only logic for empty JWT_SECRET.
  • Add unit tests covering secret generation, behavior with configured/short secrets, RS256 configs, and uniqueness of generated secrets.
app.py
tests/unit/test_jwt_secret_bootstrap.py
Adjust tenant middleware to differentiate SaaS custom-domain behavior from self-host mode and ensure unknown hosts don’t return sitewide 404s when custom domains are disabled.
  • Extend TenantMiddleware with a custom_domains_enabled flag and change dispatch logic so unknown hosts pass through when the flag is false.
  • Wire custom_domains_enabled from settings into middleware registration in app.py.
  • Update tenant middleware unit test helper to accept custom_domains_enabled and add tests for unknown host pass-through and preserving behavior for known custom tenants in disabled mode.
middleware/tenant.py
app.py
tests/unit/middleware/test_tenant.py
Clean up JWT-related defaults in environment configuration.
  • Remove placeholder JWT keys from .env.example so no bogus RS256 configuration is implied.
  • Rely on HS256 fallback and new bootstrap behavior when JWT configuration is left empty.
.env.example

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Fixed security issues:

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path=".github/workflows/deploy.yml" line_range="42-45" />
<code_context>
+        with:
+          persist-credentials: false
+
+      - name: Upload self-host compose to release
+        env:
+          GH_TOKEN: ${{ github.token }}
+        run: gh release upload ${{ github.event.release.tag_name }} selfhost/docker-compose.yml --clobber
+
   build:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** The workflow assumes `gh` is available on the runner, which might break if GitHub changes the default image.

The `assets` job calls `gh release upload` and assumes the GitHub CLI is preinstalled on `ubuntu-latest`. To avoid brittle, image-dependent failures, consider adding an explicit install/setup step for `gh` or pinning the runner image to one that guarantees `gh` is available.
</issue_to_address>

### Comment 2
<location path="tests/unit/test_jwt_secret_bootstrap.py" line_range="41-44" />
<code_context>
+        _ensure_jwt_secret(settings)
+        assert settings.jwt.jwt_secret == secret
+
+    def test_short_secret_kept_but_not_replaced(self):
+        settings = _settings(jwt_secret="short")
+        _ensure_jwt_secret(settings)
+        assert settings.jwt.jwt_secret == "short"
+
+    def test_rs256_config_is_untouched(self):
</code_context>
<issue_to_address>
**suggestion (testing):** Also assert that a warning is emitted for short JWT secrets to lock in the security feedback behavior

This test verifies the secret is preserved, but `_ensure_jwt_secret` should also emit a `jwt_secret_weak` warning in this case. Please add an assertion on the logged warning so that any future change that removes or weakens this security signal is caught. You can reuse the logging capture pattern from the autogenerated-secret test.

Suggested implementation:

```python
    def test_short_secret_kept_but_not_replaced(self, caplog):
        settings = _settings(jwt_secret="short")

        with caplog.at_level(logging.WARNING):
            _ensure_jwt_secret(settings)

        assert settings.jwt.jwt_secret == "short"

        # Ensure the weak-secret warning is emitted so security feedback stays intact.
        # Pattern should match the autogenerated-secret test's logging assertion.
        assert any(
            record.levelname == "WARNING"
            and record.message == "jwt_secret_weak"
            for record in caplog.records
        )

```

1. Ensure `logging` is imported at the top of this test module (e.g. `import logging`) if it is not already.
2. Align the warning assertion (message text and possibly logger name) with whatever pattern is used in the existing autogenerated-secret test (e.g. checking `record.message`, `record.getMessage()`, or `record.msg`, and any specific logger name or extra fields that test may rely on).
3. If the autogenerated-secret test uses a helper or a different structure for capturing log records (such as filtering by `record.name` or checking a specific substring in the message), mirror that structure here for consistency.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +42 to +45
- name: Upload self-host compose to release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload ${{ github.event.release.tag_name }} selfhost/docker-compose.yml --clobber

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The workflow assumes gh is available on the runner, which might break if GitHub changes the default image.

The assets job calls gh release upload and assumes the GitHub CLI is preinstalled on ubuntu-latest. To avoid brittle, image-dependent failures, consider adding an explicit install/setup step for gh or pinning the runner image to one that guarantees gh is available.

Comment on lines +41 to +44
def test_short_secret_kept_but_not_replaced(self):
settings = _settings(jwt_secret="short")
_ensure_jwt_secret(settings)
assert settings.jwt.jwt_secret == "short"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Also assert that a warning is emitted for short JWT secrets to lock in the security feedback behavior

This test verifies the secret is preserved, but _ensure_jwt_secret should also emit a jwt_secret_weak warning in this case. Please add an assertion on the logged warning so that any future change that removes or weakens this security signal is caught. You can reuse the logging capture pattern from the autogenerated-secret test.

Suggested implementation:

    def test_short_secret_kept_but_not_replaced(self, caplog):
        settings = _settings(jwt_secret="short")

        with caplog.at_level(logging.WARNING):
            _ensure_jwt_secret(settings)

        assert settings.jwt.jwt_secret == "short"

        # Ensure the weak-secret warning is emitted so security feedback stays intact.
        # Pattern should match the autogenerated-secret test's logging assertion.
        assert any(
            record.levelname == "WARNING"
            and record.message == "jwt_secret_weak"
            for record in caplog.records
        )
  1. Ensure logging is imported at the top of this test module (e.g. import logging) if it is not already.
  2. Align the warning assertion (message text and possibly logger name) with whatever pattern is used in the existing autogenerated-secret test (e.g. checking record.message, record.getMessage(), or record.msg, and any specific logger name or extra fields that test may rely on).
  3. If the autogenerated-secret test uses a helper or a different structure for capturing log records (such as filtering by record.name or checking a specific substring in the message), mirror that structure here for consistency.

Zingzy added 2 commits July 5, 2026 02:07
Release images now build for linux/arm64 alongside amd64 — Apple
Silicon, Raspberry Pi, and ARM cloud boxes couldn't pull at all
(no matching manifest). Per-merge edge builds stay amd64-only for
speed; prod pulls the amd64 leg of the manifest unchanged.
Builder stage installs the locked dependencies keyed on pyproject+lock
alone, so code edits never invalidate the install layer. Runtime image
drops the dev group (pytest/ruff/pre-commit — 92MB lighter) and runs as
appuser. UV_NO_SYNC pins uv run entrypoints to the baked venv so
container starts never try to re-sync the missing dev group.
@Zingzy
Zingzy force-pushed the feat/docker-selfhost branch from 4b6b4d8 to 4b9b928 Compare July 12, 2026 21:10

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
dockerfile (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin the uv image version instead of using :latest.

Both the builder and runtime stages copy from ghcr.io/astral-sh/uv:latest. Floating :latest makes builds non-reproducible and vulnerable to breaking changes if astral-sh releases an incompatible uv version. Pin to a specific version (e.g., ghcr.io/astral-sh/uv:0.5.x) for reproducible builds.

♻️ Suggested change
-COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
+COPY --from=ghcr.io/astral-sh/uv:0.5.11 /uv /uvx /bin/

Also applies to: 32-32

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dockerfile` at line 4, Replace the floating :latest tag in both uv image
references used by the builder and runtime stages with the approved specific uv
version, keeping the existing /uv, /uvx, and /bin/ copy paths unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/deploy.yml:
- Around line 42-45: Update the “Upload self-host compose to release” step to
pass the release tag through its env configuration using a descriptive variable,
then reference that shell environment variable in the gh release upload command
instead of interpolating github.event.release.tag_name directly in run.

---

Nitpick comments:
In `@dockerfile`:
- Line 4: Replace the floating :latest tag in both uv image references used by
the builder and runtime stages with the approved specific uv version, keeping
the existing /uv, /uvx, and /bin/ copy paths unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a7ea566-6f98-4eb7-9583-2ee0d5837da1

📥 Commits

Reviewing files that changed from the base of the PR and between af81ce4 and 4b9b928.

📒 Files selected for processing (8)
  • .env.example
  • .github/workflows/deploy.yml
  • app.py
  • dockerfile
  • middleware/tenant.py
  • selfhost/docker-compose.yml
  • tests/unit/middleware/test_tenant.py
  • tests/unit/test_jwt_secret_bootstrap.py

Comment on lines +42 to +45
- name: Upload self-host compose to release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload ${{ github.event.release.tag_name }} selfhost/docker-compose.yml --clobber

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Pass tag_name through an environment variable to prevent template injection.

github.event.release.tag_name is interpolated directly into a run command via ${{ }}. While Git tag names are character-restricted (no spaces, semicolons, or backticks), this is a confirmed template-injection finding from zizmor and violates the GitHub Actions security best practice of never expanding ${{ }} expressions inside run steps. The fix is trivial — move the value into an environment variable and reference it by name.

🔒️ Proposed fix: use env var instead of direct interpolation
       - name: Upload self-host compose to release
         env:
           GH_TOKEN: ${{ github.token }}
+          RELEASE_TAG: ${{ github.event.release.tag_name }}
-        run: gh release upload ${{ github.event.release.tag_name }} selfhost/docker-compose.yml --clobber
+        run: gh release upload "$RELEASE_TAG" selfhost/docker-compose.yml --clobber
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Upload self-host compose to release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload ${{ github.event.release.tag_name }} selfhost/docker-compose.yml --clobber
- name: Upload self-host compose to release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: gh release upload "$RELEASE_TAG" selfhost/docker-compose.yml --clobber
🧰 Tools
🪛 zizmor (1.26.1)

[error] 45-45: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/deploy.yml around lines 42 - 45, Update the “Upload
self-host compose to release” step to pass the release tag through its env
configuration using a descriptive variable, then reference that shell
environment variable in the gh release upload command instead of interpolating
github.event.release.tag_name directly in run.

Source: Linters/SAST tools

@Zingzy
Zingzy marked this pull request as draft July 13, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants