Skip to content

fix: Docker security hardening and CI version alignment#712

Merged
2witstudios merged 4 commits intomasterfrom
ppg/docker-security
Feb 27, 2026
Merged

fix: Docker security hardening and CI version alignment#712
2witstudios merged 4 commits intomasterfrom
ppg/docker-security

Conversation

@2witstudios
Copy link
Owner

@2witstudios 2witstudios commented Feb 24, 2026

Summary

  • Remove secrets from Docker images: Removed COPY .env ./ from realtime, migrate, and seed Dockerfiles — env vars are injected at runtime via docker-compose env_file:. Added .env* glob to .dockerignore as defense-in-depth.
  • Realtime container runs as non-root: Added USER node instruction matching the web Dockerfile pattern.
  • Realtime multi-stage build: Converted from single-stage to proper 2-stage build (builder/runner). Builder compiles shared packages (@pagespace/db, @pagespace/lib) and the realtime service via tsc. Runner installs production-only dependencies (--prod) and copies only compiled dist/ artifacts — no devDependencies or TypeScript source in the final image.
  • Realtime targeted COPY: Builder copies only packages/db, packages/lib, and types (not the full packages/ tree). Runner copies only dist/ directories from the builder — package.json files are already present from the prod install step for pnpm workspace resolution.
  • Processor frozen lockfile: Changed --no-frozen-lockfile to --frozen-lockfile for reproducible builds.
  • Worker healthcheck: Replaced no-op console.log healthcheck with node-based PID 1 liveness check (process.kill(1, 0)) — avoids procps-ng dependency and pgrep ancestor shell match false-positives.
  • Disable Next.js telemetry: Enabled NEXT_TELEMETRY_DISABLED=1 in both builder and runner stages of the web Dockerfile.
  • CI Node.js version: Updated from Node 20 to Node 22 to match production Dockerfiles (node:22.17.0-alpine).
  • CI PostgreSQL version: Updated from postgres:16 to postgres:17-alpine to match docker-compose.yml (postgres:17.5-alpine).

Test plan

  • Verify docker compose build succeeds for all services
  • Verify docker compose up starts all services correctly with env vars from env_file
  • Confirm realtime container runs as non-root (docker exec <container> whoaminode)
  • Confirm no .env file exists inside built images
  • Verify CI pipeline passes with Node 22 and Postgres 17

🤖 Generated with Claude Code

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

The PR modernizes CI/CD infrastructure by upgrading Node.js to version 22 and PostgreSQL to 17-alpine, enforces frozen lockfiles in the processor build, restructures the realtime service into a multi-stage Dockerfile, disables Next.js telemetry, removes embedded .env files from utility Dockerfiles, and replaces the worker healthcheck with actual process verification.

Changes

Cohort / File(s) Summary
CI/Environment Configuration
.dockerignore, .github/workflows/test.yml
Added .env to ignore rules; updated Node.js from 20 to 22 and PostgreSQL image from 16 to 17-alpine in CI workflow.
Processor Dockerfile
apps/processor/Dockerfile
Changed dependency installation to use --frozen-lockfile instead of --no-frozen-lockfile, enforcing strict lockfile validation.
Realtime Multi-Stage Build
apps/realtime/Dockerfile
Restructured from single-stage to three-stage build (deps, builder, runner) with separate dependency installation, build aggregation, and production runtime stages.
Web Application Dockerfiles
apps/web/Dockerfile, apps/web/Dockerfile.migrate, apps/web/Dockerfile.seed, apps/web/Dockerfile.worker
Uncommented telemetry disable environment variables in web Dockerfile; removed .env file copies from migrate and seed utilities; replaced worker healthcheck from no-op to actual process verification using pgrep -f "file-processor".

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Dockerfiles dance in triplet stages,
Node twenty-two turns fresh new pages,
Frozen lockfiles hold their ground,
Process checks make health abound,
Build pipelines optimized for all ages! 🐳✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the primary objectives of the PR: Docker security hardening (non-root user, removed .env files, multi-stage builds) and CI version alignment (Node.js 22, PostgreSQL 17).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ppg/docker-security

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 and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cace0687fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
.dockerignore (1)

31-35: Good security improvement — consider also adding a wildcard .env* pattern.

Adding .env here correctly prevents secrets from leaking into Docker images. However, the explicit list still misses variants like .env.staging or .env.production. A single .env* glob (with a !.env.example override if you want to keep the template available) would be more robust.

This is optional since the current explicit list covers the standard Next.js variants.

♻️ Optional: replace explicit list with a glob
 # Environment files
-.env
-.env.local
-.env.development.local
-.env.test.local
-.env.production.local
+.env*
+!.env.example
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.dockerignore around lines 31 - 35, Replace the explicit .env variants in
the .dockerignore with a glob to catch all env files: update the lines that
currently list ".env", ".env.local", ".env.development.local",
".env.test.local", ".env.production.local" to include a single ".env*" entry
(and optionally add an override like "!.env.example" if you want to keep the
template in images); this ensures variants like .env.staging or .env.production
are also ignored.
apps/realtime/Dockerfile (1)

8-11: git and custom npm timeout config may be unnecessary baggage in the final image.

apk add git and the npm timeout settings were likely carried over from the old single-stage Dockerfile. Since git is only needed during dependency installation (deps stage), and these npm config lines apply to npm (not pnpm), consider whether they're still needed. The runner stage inherits none of this, but the deps stage could be leaner without git if no git-based dependencies exist.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/realtime/Dockerfile` around lines 8 - 11, Remove unnecessary tooling and
npm config from the final runner image: if your project uses pnpm and has no
git-based dependencies, delete the "apk add --no-cache git" and the "npm config
set fetch-..." lines from this RUN instruction, or move them into the
deps/install stage only (keep them in the stage that runs package installation).
Check for any git-based dependencies in package.json and whether pnpm is used;
if pnpm is used, drop the npm config lines entirely. Update the RUN that
performs installation (and the deps stage) to include git only when required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/realtime/Dockerfile`:
- Around line 36-47: The runner stage currently copies the full build output
(including node_modules with devDependencies) via COPY --from=builder /app ., so
set up the runner to perform a production-only dependency step: in the runner
stage (the Dockerfile's "runner" stage and the lines around COPY --from=builder
/app . and ENV NODE_ENV=production) copy only package.json /pnpm-lock.yaml (and
any built dist output from the builder for `@pagespace/db` and `@pagespace/lib`),
run corepack enable and a production install or prune (e.g., pnpm install --prod
or pnpm prune --prod) so only production deps are present, and then switch to
USER node; ensure the builder stage still emits built artifacts for shared
packages so the runner can install without dev toolchains.
- Around line 24-34: The builder stage currently only copies source/deps and
never compiles packages, so add explicit build steps in the builder stage to
compile shared packages and the realtime app: run pnpm --filter `@pagespace/db`
build, pnpm --filter `@pagespace/lib` build, and pnpm --filter realtime build (or
pnpm build for the workspace target) after enabling corepack and copying
sources, ensuring the produced artifacts are available for the final runtime
stage and devDependencies are not carried into the runtime image; reference the
builder stage and the runner/production copy to confirm artifacts are copied
from the builder.

In `@apps/web/Dockerfile.worker`:
- Around line 42-44: The HEALTHCHECK uses pgrep ("pgrep -f \"file-processor\"")
but the base image (node:22.17.0-alpine) doesn't include pgrep; install the
package that provides it before the HEALTHCHECK. Add a RUN apk add --no-cache
procps-ng prior to the HEALTHCHECK directive so pgrep is available when the
HEALTHCHECK (pgrep -f "file-processor") runs.

---

Nitpick comments:
In @.dockerignore:
- Around line 31-35: Replace the explicit .env variants in the .dockerignore
with a glob to catch all env files: update the lines that currently list ".env",
".env.local", ".env.development.local", ".env.test.local",
".env.production.local" to include a single ".env*" entry (and optionally add an
override like "!.env.example" if you want to keep the template in images); this
ensures variants like .env.staging or .env.production are also ignored.

In `@apps/realtime/Dockerfile`:
- Around line 8-11: Remove unnecessary tooling and npm config from the final
runner image: if your project uses pnpm and has no git-based dependencies,
delete the "apk add --no-cache git" and the "npm config set fetch-..." lines
from this RUN instruction, or move them into the deps/install stage only (keep
them in the stage that runs package installation). Check for any git-based
dependencies in package.json and whether pnpm is used; if pnpm is used, drop the
npm config lines entirely. Update the RUN that performs installation (and the
deps stage) to include git only when required.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6247d21 and cace068.

📒 Files selected for processing (8)
  • .dockerignore
  • .github/workflows/test.yml
  • apps/processor/Dockerfile
  • apps/realtime/Dockerfile
  • apps/web/Dockerfile
  • apps/web/Dockerfile.migrate
  • apps/web/Dockerfile.seed
  • apps/web/Dockerfile.worker
💤 Files with no reviewable changes (2)
  • apps/web/Dockerfile.migrate
  • apps/web/Dockerfile.seed

2witstudios and others added 2 commits February 25, 2026 18:23
Remove .env COPY from realtime, migrate, and seed Dockerfiles — env vars
are provided at runtime via docker-compose env_file. Add .env to
.dockerignore. Convert realtime Dockerfile to multi-stage build with
USER node. Enable --frozen-lockfile in processor build. Replace no-op
worker healthcheck with pgrep process check. Enable NEXT_TELEMETRY_DISABLED
in web Dockerfile. Align CI to Node 22 and Postgres 17 matching production.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…check

Realtime Dockerfile:
- Add build steps for shared packages and realtime service (tsc)
- Production-only install in runner stage (--prod) excludes devDependencies
- Remove git/npm config (only pnpm is used, no git-based deps)
- Runner now executes compiled JS via node instead of tsx

Worker healthcheck:
- Replace pgrep with node-based PID 1 liveness check
- Avoids procps-ng dependency and ancestor shell match false-positives

.dockerignore:
- Use .env* glob with !.env.example override for broader coverage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@2witstudios
Copy link
Owner Author

Result: ag-1rno7f2a

PR

(Review-only — no code changes made, no PR created)

Summary

Comprehensive code review of PR #712: "fix: Docker security hardening and CI version alignment"

Changes

Review-only. No files were modified.

Notes

See full review below.


🔬 COMPREHENSIVE CODE REVIEW: Docker Security Hardening & CI Version Alignment (PR #712)

PR Scope Analysis

Changes span 8 files (+44 / -40):

  1. .dockerignore — Simplify env file exclusion with glob
  2. .github/workflows/test.yml — Upgrade Node 20→22, Postgres 16→17-alpine
  3. apps/processor/Dockerfile — Switch to --frozen-lockfile
  4. apps/realtime/Dockerfile — Full rewrite: multi-stage build, non-root user
  5. apps/web/Dockerfile — Enable telemetry opt-out in both stages
  6. apps/web/Dockerfile.migrate — Remove COPY .env
  7. apps/web/Dockerfile.seed — Remove COPY .env
  8. apps/web/Dockerfile.worker — Replace no-op healthcheck with PID 1 liveness

Functional Requirements: PR description lists 8 objectives — all addressed in the diff.


1. Code Structure & Organization

🎯 Consistent Docker patterns across services
💡 Each Dockerfile now follows clearer separation between build and runtime
🪞 The realtime Dockerfile is the most significant change — full multi-stage rewrite

Strengths:

  • Multi-stage build for realtime matches the existing patterns in processor and web Dockerfiles
  • Removing COPY .env from 3 Dockerfiles creates consistency — all services now rely on env_file: in docker-compose
  • .dockerignore glob simplification is cleaner and more maintainable

2. Security Analysis (OWASP Top 10 Scan) ✅ with notes

Explicit review of each OWASP Top 10 category against the PR changes:

# OWASP Category Status Notes
A01 Broken Access Control ✅ N/A No access control changes
A02 Cryptographic Failures ✅ N/A No crypto changes
A03 Injection ✅ N/A No user input handling changes
A04 Insecure Design ✅ Improved Removing .env from images is a design-level security improvement
A05 Security Misconfiguration ✅ Improved Non-root user, frozen lockfile, .env removal, telemetry disabled
A06 Vulnerable/Outdated Components ✅ Improved Node 22 + Postgres 17-alpine alignment
A07 Auth Failures ✅ N/A No auth changes
A08 Software/Data Integrity ✅ Improved --frozen-lockfile ensures reproducible builds
A09 Logging/Monitoring Failures ✅ N/A No monitoring changes
A10 SSRF ✅ N/A No request forwarding changes

Security-specific findings:

.env Removal from Docker Images (Excellent)

Baking .env into images is a significant secret-leakage risk. This PR correctly removes COPY .env ./ from:

  • apps/realtime/Dockerfile
  • apps/web/Dockerfile.migrate
  • apps/web/Dockerfile.seed

All three services have env_file: .env in docker-compose.yml, so env vars are injected at runtime. Defense-in-depth via .env* in .dockerignore is good.

✅ Non-Root Container Execution (Realtime)

USER node in the runner stage is correct and matches the web Dockerfile pattern. The node user (UID 1000) is built into the node:*-alpine base image.

.dockerignore Glob Pattern

.env* with !.env.example exception is correct. Note: this also catches .envrc (direnv) which is fine for Docker context.


3. Docker Best Practices Assessment

Multi-Stage Build (Realtime) — Well Structured

Builder stage:

  • Correctly installs all deps (--prod=false) for compilation
  • Only copies apps/realtime source (not apps ./apps) — good scope reduction
  • Builds shared packages (@pagespace/db, @pagespace/lib) then realtime service

Runner stage:

  • Production-only deps (--prod)
  • Copies built artifacts from builder
  • Sets NODE_ENV=production
  • Runs as non-root node user

⚠️ FINDING: Realtime CMD may fail at runtime — CJS/ESM mismatch risk

Severity: Medium — Potential runtime failure

The PR changes the realtime CMD from:

CMD ["pnpm", "--filter", "realtime", "start"]  # tsx src/index.ts (ESM-aware)

to:

CMD ["node", "apps/realtime/dist/index.js"]  # Plain node (CJS output)

The realtime tsconfig.json compiles to "module": "commonjs". The source code uses ESM import syntax which tsc will transpile to require() calls.

The concern: The workspace packages @pagespace/db and @pagespace/lib both build to CommonJS ("module": "commonjs" in their tsconfig.build.json), so the compiled require() calls should resolve correctly through the "main": "./dist/index.js" fields. However, there are two risks:

  1. dotenv.config({ path: '../../.env' }) on line 22 of index.ts — In the compiled runner at /app/apps/realtime/dist/index.js, this resolves to /app/.env which won't exist (since .env is no longer copied). This is actually fine because env vars are injected via docker-compose, but dotenv will silently fail to find the file. No breakage, but dead code should be noted.

  2. cookie dependencycookie package is listed in realtime's dependencies (not devDependencies), so it should be installed with --prod. This is correct.

Verdict: The CJS compilation should work. The packages all export CommonJS. The main risk is if any transitive dependency uses ESM-only features, but that would also fail with tsx in development. Low risk but should be verified with docker compose build && docker compose up.

Processor Frozen Lockfile — Correct Fix

Switching from --no-frozen-lockfile to --frozen-lockfile is the right call. The old comment said "allow lockfile update since processor deps changed" — that was a workaround that shouldn't persist. Reproducible builds require frozen lockfiles.

Worker Healthcheck Improvement — Good

Old healthcheck:

CMD node -e "console.log('Worker health check')" || exit 1

This always passes — it just verifies node can execute, not that the worker process is alive.

New healthcheck:

CMD node -e "try { process.kill(1, 0); } catch(e) { process.exit(1); }"

process.kill(pid, 0) sends signal 0 (no-op liveness probe) to PID 1 (the container's main process). If PID 1 is gone, process.kill throws, and the healthcheck fails. This is a legitimate liveness check with no extra dependencies.

Telemetry Disabled — Good Hygiene

Enabling NEXT_TELEMETRY_DISABLED=1 in both builder and runner stages prevents data collection during builds and at runtime. The =1 syntax (vs the old space-separated ENV NEXT_TELEMETRY_DISABLED 1) follows the modern Dockerfile best practice.


4. CI Version Alignment

✅ Node 20 → 22

Production Dockerfiles use node:22.17.0-alpine. CI was on Node 20. This alignment prevents "works in CI but not in production" issues.

✅ Postgres 16 → 17-alpine

docker-compose.yml uses postgres:17.5-alpine. CI was on postgres:16. Now aligned.

Note: The CI uses postgres:17-alpine (floating tag) while docker-compose uses postgres:17.5-alpine (pinned). This is acceptable — CI gets the latest 17.x patch, production pins a specific version for reproducibility. Minor drift is expected and not a concern.


5. Potential Issues & Recommendations

⚠️ Issue 1: Dead dotenv.config() Call in Realtime

File: apps/realtime/src/index.ts:22
Impact: Low (benign, no breakage)

dotenv.config({ path: '../../.env' });

With the multi-stage build running node apps/realtime/dist/index.js from /app, the relative path ../../.env resolves to a location outside the container. In the old single-stage build with COPY .env ./, this loaded env vars from the copied file. Now that env vars come from docker-compose env_file:, this call does nothing but silently fail.

Recommendation: Consider removing the dotenv.config() call in a follow-up PR, or guarding it for development only. It's not a blocker for this PR.

⚠️ Issue 2: Realtime Runner Copies apps/web/package.json But Not apps/web Source

Impact: Low (pnpm workspace resolution)

The runner stage copies:

COPY --from=builder /app/apps/web/package.json ./apps/web/

This is needed for pnpm workspace resolution (since pnpm-workspace.yaml references apps/web), but no apps/web source or dist is copied. This is correct — it's a manifest stub for workspace dependency resolution. Worth a brief comment in the Dockerfile for future maintainers.

⚠️ Issue 3: Missing git in Realtime Builder

Impact: Low — only if dependencies require git during install

The old Dockerfile installed git (apk add --no-cache git). The new multi-stage builder does not. If any npm dependency uses git:// protocol in its resolution, the install will fail. Since the lockfile already resolves all URLs, this is unlikely to matter. Monitor the first build.

⚠️ Issue 4: Missing npm config set Timeout Tweaks

Impact: Low

The old Dockerfile had:

RUN npm config set fetch-timeout 300000 && \
    npm config set fetch-retry-maxtimeout 120000 && \
    npm config set fetch-retry-mintimeout 10000

These were removed. Since pnpm is used (not npm), these npm config settings had no effect on pnpm install. Correct removal.


6. OWASP Deep Scan — No Vulnerabilities Found

  • No secrets in code: No API keys, tokens, or passwords visible in any changed file
  • No command injection: Dockerfile commands use exec-form CMD ["node", ...] not shell-form
  • No privilege escalation: USER node properly drops privileges
  • Supply chain: --frozen-lockfile prevents lockfile tampering during builds

7. Architecture & Design Assessment

Design decisions are sound:

  1. Runtime env injection vs build-time baking: Correct pattern. Secrets belong in orchestration layer, not images.
  2. Multi-stage for realtime: Reduces image size by excluding devDependencies and source code from the final image.
  3. node direct execution vs tsx: Compiled JS runs without TypeScript overhead. Faster cold-start, smaller attack surface (no tsx/esbuild in production).
  4. PID 1 healthcheck: Lightweight, zero-dependency liveness check.

8. Quality Metrics

Category Score Notes
Security Improvement ✅ 100% .env removal, non-root, frozen lockfile
Docker Best Practices ✅ 95% Solid multi-stage, minor docs opportunity
CI Alignment ✅ 100% Node + Postgres versions matched
Backwards Compatibility ✅ 100% No behavioral changes to running services
Completeness ✅ 95% All 8 stated objectives met; dead dotenv noted

Critical Findings Summary

Strengths (Outstanding)

  1. Secrets removed from images — eliminates a common Docker anti-pattern
  2. Non-root execution — follows container security best practices
  3. Multi-stage build — proper separation of build and runtime concerns
  4. Frozen lockfile enforcement — reproducible builds across all services
  5. Healthcheck improvement — meaningful liveness probe vs. no-op
  6. CI/production parity — version alignment reduces environment drift

Areas for Improvement (None Blocking)

  1. Dead dotenv.config() in realtime — benign but should be cleaned up in follow-up
  2. Missing build verification — the realtime multi-stage build should be smoke-tested (docker compose build realtime && docker compose up realtime)
  3. Comment for web package.json stub — would help future maintainers understand the pnpm workspace requirement

Final Assessment

Overall Score: 94/100 (Excellent)

Breakdown:

  • Security: ✅ 100% — Significant hardening across all Dockerfiles
  • Correctness: ✅ 90% — High confidence but the CJS runtime switch should be verified with a build
  • Best Practices: ✅ 95% — Strong adherence to Docker multi-stage patterns
  • CI Alignment: ✅ 100% — Clean version bump with rationale
  • Completeness: ✅ 95% — All stated objectives addressed

Production Readiness: ✅ APPROVED with verification

Recommendation: Merge after verifying:

  1. docker compose build succeeds for all services (especially realtime)
  2. docker compose up — realtime service starts and accepts WebSocket connections
  3. docker exec <realtime-container> whoami returns node

The PR achieves meaningful security hardening with zero behavioral changes to running services. Clean work.

2witstudios added a commit that referenced this pull request Feb 26, 2026
- Realtime Dockerfile: copy only dist/ and package.json from packages
  instead of full packages/ directory (smaller, cleaner image)
- Realtime Dockerfile: add HEALTHCHECK for consistency with worker and
  processor services
- Realtime service: add /health HTTP endpoint to support HEALTHCHECK
- CI test.yml: pin Node 22.17.0 and postgres:17.5-alpine to match
  docker-compose
- CI load-test.yml: align Node 22.17.0 and postgres:17.5-alpine with
  test.yml and docker-compose

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2witstudios and others added 2 commits February 27, 2026 08:38
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…st artifacts

Builder stage: replace broad `COPY packages` with targeted copies of
packages/db, packages/lib, and types (needed for build-time type roots).

Runner stage: copy only compiled dist/ directories instead of full package
trees — package.json files are already present from the prod install step,
so Node module resolution via pnpm workspace links works correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@2witstudios 2witstudios merged commit b876cd3 into master Feb 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant