::materialize-file-branch persists a branch's derived state as the
file's own data payload, drops the op log, releases the pinned base
snapshot, closes any pull request over it and clears the branch marker,
so every read and write path takes the ordinary route from then on.
That is the preview's exit door: the flag can go off and the engine can
be replaced without stranding anybody with an unreadable file.
It keys on the branch file rather than the branch metadata, so calling
it twice is a reported no-op rather than an error.
The fetch the cache exists for is a file open, and expireAfterAccess of
ten minutes threw the entry away between two sessions on the same file:
a listing measured at 5 ms was 26 s again fifteen minutes later. An
entry is a uuid and five numbers, so the window costs nothing.
The listing's per-branch [ahead behind conflicts] summary is cached in
memory keyed by (base-snapshot-id, source-revn, branch-revn), which
names exactly the inputs the computation reads. A stale key cannot
exist, so the cache needs no invalidation logic: a repeat listing with
no intervening save performs no comparison work, and a cache miss is
the old behaviour. Failed computations are not stored, so a broken
branch keeps degrading to zeros with a warning.
The technical guide documented the full-copy branch model this change
replaces. Update the conceptual model, data model, creation steps,
ahead/behind computation, update-from-main (squash), and the limitations
section to describe the derived-state model.
Swap the storage and materialization layer of file branching to remove
the per-branch data duplication, keeping the RPC contracts and the
frontend unchanged.
- file_branch_change (0154): one row per branch save, the blob-encoded
change vector, ordered by revn; branch files store no data payload.
- Read path: app.binfile.common/get-file* derives the branch file data
by replaying the op log over the pinned merge-base snapshot, carrying
the base's version and migration set so need-migration? stays quiet.
- Save path: update-file routes branch files to persist-branch-file!,
which appends the vector to the op log and updates the file row
without any file_data write.
- Creation: create-file-branch no longer duplicates main's data; it
inserts an empty branch file row, grants the creator, and copies the
library rels.
- update-branch-from-main squashes the op log after integrating main's
changes: the repositioned base already carries main's side, so the
log is replaced with the net branch-only changes.
- Kept (keep-branch) branches retain the base pin: their state is
derived, so releasing the pin would leave them unreadable after GC.
Tests: backend-tests.rpc-file-branch-oplog-test asserts the storage and
derivation invariants; the existing branch, file, binfile, snapshot,
thumbnail, and pull request suites pass unchanged against the swapped
engine (84 tests, 1487 assertions, 0 failures).
This commit carries the design-branching preview by Juan de la Cruz:
elhombretecla/penpot:branching at 0ac265a3f5, which holds 45 commits
and 3 develop merges and was last synced with develop on 2026-09-16.
The branch is squashed together with a merge of origin/develop at
7c039231f7, so the tree is the merged result.
The merge resolved conflicts in six files:
- backend/src/app/migrations.clj: develop's 0155 and 0156 steps run
first, then the branching steps; the names differ.
- backend/src/app/rpc/commands/viewer.clj: both requires stay.
- common/src/app/common/flags.cljc: both flags stay.
- frontend/src/app/main/ui/dashboard/file_menu.cljs: "Create branch"
becomes a menu-item* with the same id, guard and handler.
- frontend/src/app/main/ui/dashboard/grid.cljs: the portal goes with
develop's menu rewrite; the branches popover stays in the trigger.
- frontend/translations/en.po: both sides' entries stay, with
develop's source references.
Co-authored-by: Juan de la Cruz <delacruzgarciajuan@gmail.com>
AI-assisted-by: mixed models
* 🎉 Move backend jobs to the unified job table substrate
Replace the generic 'task' table with a unified 'job' table as the
durable substrate for all backend jobs, and migrate every producer
and consumer to it. The legacy 'task' table stops receiving writes
but stays dormant (still cleaned by tasks-gc); upload-session
chunking and the other post-branch upstream changes are preserved.
New 0154 migration creates the 'job' table: dispatch and lifecycle
columns (name, queue, priority, scheduled_at, retries, status,
timestamps, props) plus optional user-facing ledger columns
(profile_id, target, progress, error, result, resource_id,
expires_at). No modified_at trigger: the application updates it on
every write for lease and orphan detection. Partial indexes cover
the dispatcher claim query, orphan scanning, and per-profile
listing.
New app.jobs public API: submit!/invoke!, heartbeats and progress
reporting, a precompiled job-def registry (name, schema, handler,
decoder, validator), an ephemeral request/response mode, and a
management API for external workers. The dispatcher claims
new/retry rows (FOR UPDATE SKIP LOCKED) with a plain JSON redis
payload and marks lease-expired running rows as orphaned; the
runner executes per-queue with the same retry conventions.
New jobs GC (daily): deletes expired rows and retains internal
terminal rows per :jobs-retention, touching referenced storage
objects so storage-gc-touched reclaims them. Job resources live in
a dedicated job-resource bucket with a reclaim branch in
storage-gc-touched.
All production call-sites (sendmail, delete-object, demo-purge,
file-gc, offload, webhooks, quotes, nitrate bulk delete,
snapshots, telemetry) and the ten cron tasks now submit through
app.jobs; cron is a pure scheduler and the legacy worker registry,
task-table inserts, and per-namespace handlers are removed.
Includes migrations, dispatcher, runner, jobs, request/response,
jobs GC, management API, and cron test suites.
AI-assisted-by: glm-5.3-flash
AI-assisted-by: muse-spark-1.3-contributor
AI-assisted-by: kimi-k3
AI-assisted-by: mimo-v2.5
* ✨ Address review comments on jobs substrate
Resolves the four open review threads on the jobs PR. Orphaned jobs
are now marked aborted, a system-side terminal state with no retry:
the dispatcher sweep stores the end event with outcome aborted in a
single bulk insert in the same transaction, records the terminal
outcome next to the orphaned counter, and reports an error log;
orphans keep alerting through the log and the counter, with no
user notification.
The aborted state is terminal everywhere: the status CHECK, the
sweep, the lifecycle writers, the jobs GC retention, the backlog
gauges and the user status mapping, with tests covering each
transition. The claim comment, the 2.20 doc version and the legacy
task table notes are corrected, and objects-gc heartbeats per chunk.
AI-assisted-by: muse-spark-1.3-contributor
actions/create-github-app-token v3 deprecates the app-id input in
favour of client-id. Switch to the new TRIAGE_APP_CLIENT_ID secret.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Default is now no `!` for new functions. Existing `!`
stays as legacy until its gradual refactor.
Allowed cases live in clojure/idioms: mutating public
app.db API and functions working over transients. Any
further case must be registered there first.
AI-assisted-by: muse-spark-1.3-contributor
Kaocha finds every test namespace on the JVM, but the JS runner only
runs the ones listed in runner.cljc. Sixteen namespaces were missing
from both lists, and files.shapes-builder-test was required but not
listed, so 125 tests never ran in JS. Add them to both lists, sorted.
The only one that failed in JS was make-tokens-status-from-tokens-lib:
(.sets base-lib) is a method call in ClojureScript. On the JVM it read
the field, but (:sets ...) on the result gave nil, so the legacy lib
was built without sets or themes. Use (.-sets base-lib) and
(.-themes base-lib), as the first part of the same test does.
AI-assisted-by: claude-opus-5-5
* ✨ Add rich HTML paste to the v3 text editor
Pasting HTML into the v3 text editor keeps bold, italic, decoration and
text-transform, and drops fonts, colors and links. Gated behind
text-editor-wasm/v1-html-paste.
Refs #10465
AI-assisted-by: claude-opus-5-5
* ✨ Paste HTML on the canvas as a styled text shape
With v3 HTML paste on, pasting HTML on the canvas creates a text shape
with the same emphasis. Clipboard options are now read at paste time.
Refs #10465
AI-assisted-by: claude-opus-5-5
* 🎉 Add multi-file session routing to the MCP server
Session routing:
- Track each user's Penpot connections by session ID.
- Dispatch explicit sessions directly; otherwise discover the sole session.
- Return a retryable error when discovery fails or is incomplete.
Plugin and tools:
- Initialize connections with a fresh session ID and file metadata.
- Share the optional sessionId schema and place it last in tool inputs.
Validation:
- 84 tests, server type checking, and MCP formatting checks pass.
- Disable test logging to avoid the logging worker shutdown hang.
AI-assisted-by: gpt-6
* 🎉 Support independent MCP connections
- Require explicit connection intent for each integrated workspace.
- Derive short session IDs from the Penpot app instance and file.
- Show and copy session IDs in the MCP menu and standalone plugin UI.
- Stop stale callbacks and reconnect attempts after disconnect.
Validate with frontend and plugin tests, type checking, live multi-tab
checks, and standalone UI checks with a simulated connection. Server
tests pass with --test-force-exit; the normal runner can hang on shutdown.
AI-assisted-by: gpt-6
* 📚 Document multi-file MCP sessions
- Explain independent connections, session selection, and copying IDs.
- Describe session ID lifetimes for integrated and standalone plugins.
- Correct browser focus, connection, and recovery guidance.
Validate with the documentation site build and diff checks.
AI-assisted-by: gpt-6
* ✨ Change behavior on session ID duplication
* ✨ Open MCP toolbar menu on hover and match menu style
---------
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
Co-authored-by: elhombretecla <delacruzgarciajuan@gmail.com>
wrangler stays an ad hoc install in the plugin deploys and the devenv
image keeps its floating tag: both are deliberate, so mark them as
accepted instead of pending.
AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Translations that touch no group-like shape or ancestor do not need
the builder objects updated, since only parent resizing reads them.
AI-assisted-by: claude-opus-5-5
A git ref accepts characters that a Docker tag does not, such as "/" in
feature/foo, which made the promote and retag steps fail. Tag images with
a slug of the ref instead (anything outside [A-Za-z0-9._-] becomes "-",
at most 100 chars), as payments already does. develop, staging and
release tags are unchanged.
AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Add a CODEOWNERS entry for .github/, so changes to workflows, CI
configuration and repository automation need a review from the
ci-maintainers team.
AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Dependabot PRs get the dependencies, team infra and no issue required
labels, and auto-label no longer adds them to the triage board.
AI-assisted-by: Claude
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
WASM receives only the transforms that differ from the parent (bool
operands and masks are kept), rebuilds tiles once per moved subtree
and invalidates only the previous frame coverage.
Collect children ids into one transient vector and track WASM shape
changes in a JS Map instead of a volatile persistent map.
AI-assisted-by: claude-opus-5-5
* Make the scope of the usage instructions clear (local usage)
* Provide overview of the server's different modes of operation
* Improve npm release instructions
* Remove deprecated multi-user mode documentation
* ✨ Gate error reporters behind :error-reporting flag
Error reporters used to start on every boot with no way to
switch them off. They now only start when the new
:error-reporting flag is on (off by default, enable with
PENPOT_FLAGS=enable-error-reporting). The database reporter
needs the flag alone; mattermost needs the flag plus its
webhook url. Shutdown with the flag off is safe, and the
database loop now honors its runtime switch.
AI-assisted-by: muse-spark-1.3-contributor
* 📚 Add audit-log reference memory and update dev tooling
Adds the top-level audit-log reference memory with all event
flows and turns backend/audit-log into a redirect stub.
Also includes the staged dev-tooling updates (playwright
deps, mdts args, gitignore).
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Assign server-side initiator to frontend audit events
Frontend audit events now carry a server-assigned initiator
derived from the x-client header (penpot-frontend goes to
app, penpot-admin-console goes to admin-console, legacy
penpot-nitrate stays on admin-console, all else goes to
app). Client-sent values are always overwritten, and the
key survives on telemetry shadow rows.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Keep initiator on frontend telemetry context
The strips-pii-keys test still asserted the old contract
(initiator stripped). The initiator only names the sending
app, so it is not personal data and must survive the
telemetry filter like on the backend channel.
AI-assisted-by: muse-spark-1.3-contributor
* 📚 Document initiator on PostHog and browser origins
Complete the audit-log memory for the latest changes: the
initiator property forwarded to PostHog on both channels
and the eventOrigin rule for admin-console browser events.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix unscaped argument in script
---------
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
Walk only from the top-most transformed shapes, carry the component
root down the walk and memoize each shape transform so the update
step reuses it.
AI-assisted-by: claude-opus-5-5
Mark each remaining ignore in zizmor.yml as a pending clean-up or an
accepted risk, with its reason.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Deploy workflows must not restore a cache that other workflows can
write to. The pnpm store cache is removed from the api-doc and
styles-doc deploys; installs are slower but deploys are infrequent.
Also install with --frozen-lockfile in the three plugin deploys, as CI
already does by default, so a deploy never resolves dependencies that
differ from the lockfile.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Request a token for this repository only, instead of every repository
in the installation, and only with the permissions the job uses: issues
(labels) and organization projects (the board). Also document why the
workflow needs pull_request_target and why it is safe.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
The job only reads commit messages, which works with the pull_request
trigger for PRs from forks too. Running it on both triggers checked every
PR twice, once with a privileged token it does not need.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Add an explicit permissions block to every workflow that relied on the
repository default. Entry points that only call reusable workflows get
permissions: {} and grant each call what the called workflow needs:
contents: read for the bundle and docker builds, contents: write for the
release, nothing for the admin-console dispatch (it uses its own token).
Workflows that check out code get contents: read; the commit checker
also gets pull-requests: read to list the PR commits.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
* ✨ Isolate backend-test database per devenv instance
Each wsN now gets its own backend-test database
(penpot_test_wsN) and Valkey DB (6+N) on the shared
infra, so parallel test runs no longer wipe each
other. The main database stays shared on purpose.
manage.sh creates the test database on bring-up and
passes PENPOT_TEST_* into the main container. Test
helpers already read those vars, so no runtime code
changes. scripts/psql and db-schema gain --ws.
AI-assisted-by: muse-spark-1.3-contributor-free
* 🐛 Fix per-instance test database creation on bring-up
psql -c does not reliably mix SQL with psql-only
commands, so the CREATE DATABASE ... WHERE NOT
EXISTS ... \gexec one-liner never ran. Use two
plain SQL round-trips instead (check pg_database,
then CREATE DATABASE) and print what happens, so a
future failure shows up in the bring-up output.
AI-assisted-by: muse-spark-1.3-contributor-free
Run the audit archive cron when :nexus or :admin-console is on. Ship
allowlisted events to Admin Console first (with row ids for
idempotency), then the full chunk to Nexus when :nexus is set. Mark
archived_at for the whole chunk on success, including nitrate-only
mode. Rename the gate flag from :audit-log-archive to :nexus.
AI-assisted-by: Composer
Co-authored-by: Cursor <cursoragent@cursor.com>