Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions .github/.trivyignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
CVE-2026-26996
CVE-2026-27903
CVE-2026-27904
# Debian packages of the yhub image (node:22-trixie-slim) for which Debian has
# not published a fix yet. Remove an entry as soon as the scan passes without it.

# util-linux
CVE-2026-76642
CVE-2026-78408
CVE-2026-78409
CVE-2026-78410
# acl
CVE-2026-54369
# systemd
CVE-2026-16742
# ncurses
CVE-2025-69720
# perl
CVE-2026-9538
3 changes: 1 addition & 2 deletions .github/workflows/docker-hub.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ jobs:
file: src/yhub-server/Dockerfile
target: yhub
should_push: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'preview') }}
# no docker_user: the image defaults to uid 1000, the `node` user the
# base image already declares in /etc/passwd
docker_user: 1000

notify-argocd:
needs:
Expand Down
124 changes: 67 additions & 57 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,95 +6,99 @@ and this project adheres to

## [Unreleased]

## [v6.0.0] - 2026-10-05

### Added

- πŸ‘·(ci) lint, typecheck, build and test the collaboration server (yhub)
- πŸ‘·(ci) check the load-test tooling: swarm, canary, k6 and dashboards
- πŸ‘·(ci) cancel the superseded runs of a pull request
- πŸ‘·(ci) lint, typecheck, build and test the collaboration server (yhub) #2757
- πŸ‘·(ci) check the load-test tooling: swarm, canary, k6 and dashboards #2757
- πŸ‘·(ci) cancel the superseded runs of a pull request #2757
- ✨(backend) add mention endpoint with cooldown-limited email
notification #2447
- 🚩(setting) add feature flag on Duplicate with Children #2721
- πŸ’„(frontend) redesign 404 error standalone page #2696
- πŸ’„(frontend) redesign 403 access denied page #2720
- ✨(frontend) duplicate with subdocuments #2584
- ✨(frontend) add markdown download option #2608
- ✨(frontend) turn pasted doc links into interlinks #2713
- ✨(helm) allow disallowing search engine indexing per instance #2694
- πŸ›(frontend) reduce PostHog volume from web vitals and opt_in spam #2701
- ✨(backend) expose the attachment max size in the config endpoint #2577
- ✨(frontend) warn before uploading an attachment over the size limit #2577
- ✨(loadtest) add a websocket load generator for the collaboration server
- ✨(loadtest) add a websocket load generator for the collaboration
server #2556
- ✨(loadtest) add k6 scenarios for the page-open sequence and the heavy
endpoints
- ✨(loadtest) add browser canaries measuring what a user feels under load
endpoints #2556
- ✨(loadtest) add browser canaries measuring what a user feels under
load #2556
- ✨(loadtest) add the grafana dashboards of the load-test campaign, valkey
included
- πŸ“(documentation) add the load-testing guide
included #2556
- πŸ“(documentation) add the load-testing guide #2556
- πŸ”§(helm) run grafana with those dashboards in the dev cluster, in place of
the prometheus console
the prometheus console #2556
- ✨(backend) measure the calls to yhub and to the converters, the database
pool and the celery queue
pool and the celery queue #2556
- ✨(backend) add a `LoadTest` configuration and its `loadtest` application,
minting sessions for load tests
minting sessions for load tests #2556
- ✨(collaboration) add opt-in prometheus metrics to yhub, server and worker,
protected by a bearer token
protected by a bearer token #2556
- ✨(collaboration) report yhub errors to sentry, configured through
`SENTRY_*`
`SENTRY_*` #2556
- ✨(backend) add opt-in prometheus metrics on `/metrics`, protected by a
bearer token
bearer token #2556
- ✨(helm) add a dedicated ingress for the prometheus metrics of the backend
and of yhub
and of yhub #2556
- ✨(helm) add a ServiceMonitor and a PodMonitor per component whose metrics
are enabled, and `backend.metrics.enabled`
are enabled, and `backend.metrics.enabled` #2556
- πŸ”§(helm) scrape the metrics of the dev cluster with a trimmed
kube-prometheus-stack and the ServiceMonitors of the chart
kube-prometheus-stack and the ServiceMonitors of the chart #2556
- πŸ”§(helm) serve the example console of django-prometheus on the dev
Prometheus
- ✨(backend) add a service generating cached RS256 JWT tokens
- ✨(backend) publish the JWT public key on a JWKS endpoint
- πŸ”§(dev) generate the JWT signing key when bootstrapping the dev stack
- ✨(collaboration) add an admin reset-connections endpoint on yhub
- ✨(collaboration) add a create-ydoc endpoint on yhub
- πŸ”§(backend) fine tune redis cache options
- ✨(frontend) make the full last-update date available #1215
- ✨(collaboration) soft-migrate legacy S3 documents into yhub
- ✨(collaboration) replay legacy s3 version history into yhub
- ✨(backend) add a service to call the yhub REST API
- ✨(backend) call YHubService to seed initial document content
- ✨(collaboration) add a get-ydoc endpoint on yhub
- ✨(backend) duplicate a document through the collaboration server
- ✨(backend) serve `documents/{id}/formatted-content/` from yhub
- ✨(collaboration) notify the backend when the worker persists new content
- πŸ›(frontend) stop reconnecting to the websocket based on the status code
- ✨(collaboration) let a user read the document's editing history
- ✨(frontend) fall back to http polling when the websocket cannot be opened.
Prometheus #2556
- ✨(backend) add a service generating cached RS256 JWT tokens #2556
- ✨(backend) publish the JWT public key on a JWKS endpoint #2556
- πŸ”§(dev) generate the JWT signing key when bootstrapping the dev stack #2556
- ✨(collaboration) add an admin reset-connections endpoint on yhub #2556
- ✨(collaboration) add a create-ydoc endpoint on yhub #2556
- ✨(collaboration) soft-migrate legacy S3 documents into yhub #2556
- ✨(collaboration) replay legacy s3 version history into yhub #2556
- ✨(backend) add a service to call the yhub REST API #2556
- ✨(backend) call YHubService to seed initial document content #2556
- ✨(collaboration) add a get-ydoc endpoint on yhub #2556
- ✨(backend) duplicate a document through the collaboration server #2556
- ✨(backend) serve `documents/{id}/formatted-content/` from yhub #2556
- ✨(collaboration) notify the backend when the worker persists new
content #2556
- ✨(collaboration) let a user read the document's editing history #2556
- ✨(frontend) fall back to http polling when the websocket cannot be
opened. #2556
- πŸ”§(collaboration) make the version-history granularity configurable through
`COLLABORATION_VERSION_GRANULARITY_MS`
`COLLABORATION_VERSION_GRANULARITY_MS` #2556
- ✨(frontend) keep a local copy of documents, so they open and stay editable
offline
- πŸ›(frontend) stop the service worker from caching the collaboration server's
rest api
offline #2556

### Changed

- πŸ›‚(backend) let users allowed to comment list each other's accesses #2447
- ♻️(collaboration) migrate the collaboration server from hocuspocus to yhub
- πŸ’₯(y-provider) y-provider becomes converter-only
- ♻️(collaboration) migrate the collaboration server from hocuspocus to
yhub #2556
- πŸ’₯(y-provider) y-provider becomes converter-only #2556
- πŸ’₯(backend) move the resource server JWKS from `/api/{version}/jwks` to
`/external_api/{version}/jwks`
- πŸ”§(collaboration) adapt docker stack for development purpose
- πŸ”§(helm) run a valkey for the backend and one for yhub in dev and feature
- ✨(frontend) turn pasted doc links into interlinks #2713
`/external_api/{version}/jwks` #2556
- πŸ”§(collaboration) adapt docker stack for development purpose #2556
- πŸ”§(helm) run a valkey for the backend and one for yhub in dev and
feature #2556
- πŸ’„(frontend) redesign 401 error standalone page #2716

### Fixed

- πŸ›(frontend) never list two versions ending at the same moment
- πŸ›(documentation) fix minio port and keycloak realm in the k8s guide #2751
- πŸ›(collaboration) bound the calls yhub makes to the backend with a timeout,
`YHUB_BACKEND_REQUEST_TIMEOUT_MS`, 5s by default #2753
- πŸ›(frontend) cache hashed `/_next/static/` assets and revalidate HTML #2770
- πŸ›(backend) compensate document duplication failures #2755
- πŸ›(frontend) open search results in a new tab with ctrl/cmd+click #2719
- πŸ›(docker) pull minio images from pgsty
- πŸ›(backend) retry the duplicate of a document on a tree path collision
- πŸ›(docker) pull minio images from pgsty #2739
- πŸ›(backend) retry the duplicate of a document on a tree path collision #2726
- πŸ›(frontend) keep caption and alignment when replacing an image #2730
- πŸ›(backend) reset the collaboration connections on every access change #2725
- ⚑️(backend) coalesce and pace the resets of collaboration connections #2725
Expand All @@ -105,18 +109,24 @@ and this project adheres to
- πŸ“(docs) fix markdown typo in installation README #2766
- πŸ›(i18n) export locale correctly #2750
- πŸ›(frontend) keep login and logout out of the service worker cache #2771
- πŸ›(frontend) reduce PostHog volume from web vitals and opt_in spam #2701
- πŸ›(frontend) stop reconnecting to the websocket based on the status
code #2556
- πŸ›(frontend) stop the service worker from caching the collaboration server's
rest api #2556

### Removed

- πŸ”₯(backend) remove the unused `CollaborationService`
- πŸ’₯(backend) remove the `documents/{id}/can-edit/` endpoint
- πŸ’₯(backend) remove the `documents/{id}/content/` endpoint
- πŸ”₯(backend) remove the document version endpoints.
- πŸ”₯(backend) remove `Document.content`
- πŸ”₯(backend) remove the unused `CollaborationService` #2556
- πŸ’₯(backend) remove the `documents/{id}/can-edit/` endpoint #2556
- πŸ’₯(backend) remove the `documents/{id}/content/` endpoint #2556
- πŸ”₯(backend) remove the document version endpoints. #2556
- πŸ”₯(backend) remove `Document.content` #2556

### Security

- πŸ”’οΈ(collaboration) stop read-only users from sharing their cursor
- πŸ”’οΈ(collaboration) stop read-only users from sharing their cursor #2556
- πŸ”’οΈ(backend) fix race conditions between actions updating a docs #2781

## [v5.7.0] - 2026-09-15

Expand All @@ -125,7 +135,6 @@ and this project adheres to
- πŸ”§(backend) fine tune redis cache options #2658
- ✨(frontend) make the full last-update date available #1215
- πŸ’„(frontend) redesign email confirmation standalone page #2601
- ✨(frontend) add markdown download option #2608

### Changed

Expand Down Expand Up @@ -1635,7 +1644,8 @@ and this project adheres to
- ✨(frontend) Coming Soon page (#67)
- πŸš€ Impress, project to manage your documents easily and collaboratively.

[unreleased]: https://github.com/suitenumerique/docs/compare/v5.7.0...main
[unreleased]: https://github.com/suitenumerique/docs/compare/v6.0.0...main
[v6.0.0]: https://github.com/suitenumerique/docs/releases/v6.0.0
[v5.7.0]: https://github.com/suitenumerique/docs/releases/v5.7.0
[v5.6.1]: https://github.com/suitenumerique/docs/releases/v5.6.1
[v5.6.0]: https://github.com/suitenumerique/docs/releases/v5.6.0
Expand Down
9 changes: 6 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Django impress

# ---- base image to inherit from ----
FROM python:3.14.6-alpine AS base
FROM python:3.14.8-alpine AS base

# Upgrade system packages to install security updates
RUN apk update && apk upgrade --no-cache
Expand All @@ -21,7 +21,7 @@ ENV UV_LINK_MODE=copy
ENV UV_PYTHON_DOWNLOADS=0

# install uv
COPY --from=ghcr.io/astral-sh/uv:0.11.10 /uv /uvx /bin/
COPY --from=ghcr.io/astral-sh/uv:0.12.23 /uv /uvx /bin/

WORKDIR /app

Expand Down Expand Up @@ -131,7 +131,7 @@ USER root:root
RUN apk add --no-cache postgresql-client

# Install development dependencies
RUN --mount=from=ghcr.io/astral-sh/uv:0.11.10,source=/uv,target=/bin/uv \
RUN --mount=from=ghcr.io/astral-sh/uv:0.12.23,source=/uv,target=/bin/uv \
uv sync --all-extras --locked

# Restore the un-privileged user running the application
Expand Down Expand Up @@ -160,6 +160,9 @@ FROM core AS backend-production
# Remove apk cache, we don't need it anymore
RUN rm -rf /var/cache/apk/*

# pip is not needed at runtime (the dependencies are installed with uv)
RUN /usr/local/bin/python -m pip uninstall --yes pip

ARG IMPRESS_STATIC_ROOT=/data/static

# Gunicorn - not used by default but configuration file is provided
Expand Down
2 changes: 2 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ the following command inside your docker container:

## [Unreleased]

## [6.0.0] - 2026-10-05

⚠️ This release replaces the collaboration server. The content of a document
does not live in the object storage anymore, it lives in that server, and the
`y-provider` that used to serve the websocket does not serve it. There is a new
Expand Down
18 changes: 17 additions & 1 deletion src/backend/core/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,26 @@ def update(self, instance, validated_data):
"""
When no data is sent on the update, skip making the update in the database and return
directly the instance unchanged.

We don't rely on the update methof from the ModelSerializer to only save
validated_data fields to not override updated value from other
serializer like the LinkDocumentSerializer. A race condition triggering both
serializer in the same time can lead to an overlap and data can be lost.
"""
if not validated_data:
return instance # No data provided, skip the update
return super().update(instance, validated_data)
for attr, value in validated_data.items():
setattr(instance, attr, value)
try:
# `updated_at` is explicit because it is an `auto_now` field: it has
# no validated_data entry and would not be written otherwise.
instance.save(update_fields=[*validated_data, "updated_at"])
except Exception:
# The in-memory instance may hold values the database has not
# accepted: reload them so the response reports the row as it is.
instance.refresh_from_db(fields=validated_data.keys())
raise
return instance


class SearchDocumentSerializer(ListDocumentSerializer):
Expand Down
26 changes: 12 additions & 14 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
)
from core.services.yhub_services import YHubError, YHubService
from core.tasks.access import reset_service_connections_on_commit
from core.tasks.documents import sync_service_deletions_in_cascade
from core.tasks.documents import sync_service_restorations_in_cascade
from core.tasks.mail import send_ask_for_access_mail, send_mention_notification_mail
from core.tasks.search import trigger_batch_document_indexer
from core.utils.analytics import PosthogEventName, posthog_capture
Expand Down Expand Up @@ -795,13 +795,7 @@ def perform_destroy(self, instance):
"""Override to implement a soft delete instead of dumping the record in database."""
instance.soft_delete()

# the collaboration server holds the content: until it is told, it goes
# on serving the document to the clients editing it. On commit, because
# the task reads back what was just written to know what to report β€” it
# would find the document alive and restore it instead
transaction.on_commit(
partial(sync_service_deletions_in_cascade.delay, str(instance.id))
)
reset_service_connections_on_commit(instance.pk)

posthog_capture(
PosthogEventName.DOC_DELETED, self.request.user, {}, document=instance
Expand Down Expand Up @@ -1095,11 +1089,12 @@ def restore(self, request, *args, **kwargs):
except RuntimeError as err:
raise drf.exceptions.ValidationError({"detail": str(err)}) from err

# the counterpart of the deletion: the same walk puts back the content
# of the documents that came back with this one, and it reads the
# restored state back, hence on commit as well
# A soft deletion is no longer reported: the room should never be
# tombstoned, and this safety net is what lifts one should any be laid
# out of band. Documents of the subtree deleted on their own stay
# deleted, and the restored state is read back: on commit.
transaction.on_commit(
partial(sync_service_deletions_in_cascade.delay, str(document.id))
partial(sync_service_restorations_in_cascade.delay, str(document.id))
)

return drf_response.Response(
Expand Down Expand Up @@ -1897,7 +1892,7 @@ def link_configuration(self, request, *args, **kwargs):

# saving a changed link definition is what tells the collaboration
# server to re-check the connections of the document and its descendants
serializer.save()
serializer.save(update_fields=["link_reach", "link_role", "updated_at"])

return drf.response.Response(serializer.data, status=drf.status.HTTP_200_OK)

Expand Down Expand Up @@ -2024,7 +2019,10 @@ def attachment_upload(self, request, *args, **kwargs):

# Make the attachment readable by document readers
document.attachments.append(key)
document.save()
# Only the attachments column is written: the instance was loaded at the
# start of the request and an overlapping change must not be undone
# (e.g. a withdrawal of the link configuration committed in between).
document.save(update_fields=["attachments", "updated_at"])

malware_detection.analyse_file(key, document_id=document.id)

Expand Down
Loading
Loading