Compare commits

...
Author SHA1 Message Date
Eva Marco fc55bc2a48 🐛 Fix typography on shortcuts list 2026-08-06 13:41:58 +02:00
Eva Marco eb34e1c118 🐛 Fix delete path node 2026-08-06 13:38:10 +02:00
Andrey AntukhandLuis de Dios 81e44afbe3 🐛 Add backend password validation with complexity rules and dictionary check (#11059)
* 🐛 Add backend password validation with complexity rules and dictionary check

Enforce minimum 8-character password length, require at least 1 lowercase
letter, 1 uppercase letter, 1 digit, and 1 special character, and reject
common passwords using Passay library with a 10k-entry wordlist from
SecLists during registration and password change flows.

AI-assisted-by: mimo-v2.5-pro

*  Improve user feedback

When the password is invalid, the user now gets extra indications to make it stronger, so it can be valid.

* 🐛 Fix remove unneeded common password check

The dictionary check is only relevant for passwords that meet all other requirements, but all 10,000 common passwords would fail the character requirements, so this check is not needed

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-08-06 10:17:14 +02:00
Andrey Antukh 495e9f059e 🐛 Add minor fix on token tests 2026-08-06 09:35:21 +02:00
Andrey Antukh a60b648c6c 🐛 Fix issues with draft-js tests 2026-08-06 08:55:58 +02:00
Andrey Antukh b6656ee8dd 🐛 Enable SSRF check for organization SSO provider (#11064) (#11065)
Remove :skip-ssrf-check? true from prepare-organization-sso-provider so
SSRF protection is active when validating organization SSO configs.

The endpoint is already protected by shared-key authentication
(admin-console), but enabling SSRF protection prevents potential misuse
of internal network resources if the shared key were ever compromised
(defense-in-depth).

Add test prepare-organization-sso-provider-does-not-skip-ssrf-check to
verify the SSRF check is not skipped.

AI-assisted-by: qwen3.7-plus
2026-08-05 21:54:33 +02:00
Andrey Antukh 86aaf642b6 🐛 Fix scripts/ci issue with backend lintig 2026-08-05 21:52:59 +02:00
Andrey Antukh c4dd04353f 🐛 Sanitize SVG files on upload to prevent XSS
Add sanitize-svg function that removes dangerous elements and attributes:
- script tags
- foreignObject elements
- Event handler attributes (onload, onmouseover, etc.)
- javascript: URLs from href/xlink:href attributes

Apply sanitization in process-main-image before storing SVG files.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 21:52:59 +02:00
Andrey Antukh 0ac711aa68 🐛 Normalize string inputs to prevent unfiltered echo (#11061)
Add normalize-string helper in app.common.data that trims whitespace
and returns empty string for nil input. Apply to profile, team, and
project string fields (fullname, lang, theme, name) before storage.

AI-assisted-by: qwen3.7-plus
2026-08-05 17:54:07 +02:00
Andrey Antukh bf62e59f73 🐛 Add cooldown to prevent duplicate invitation emails (#11063) 2026-08-05 17:53:15 +02:00
Andrey Antukh 5906312dff 🐛 Normalize error response on duplicate file ID (#11050)
Capture unique constraint violation in insert-file! and return
generic :not-found error instead of propagating raw PostgreSQL
exception, preventing file existence oracle.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:52:14 +02:00
Andrey Antukh 25066c2f46 🐛 Require file read permissions for asset endpoints (#11036)
Add authorization check to generic-handler in assets.clj so that
/assets/by-file-media-id/:id and its /thumbnail variant verify the
requesting profile has read access to the parent file. Return 404
(not 403) when access is denied to avoid confirming existence.

Also switch get-file-media-object from db/get to db/get* so that
non-existent media objects return nil instead of raising.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:44:01 +02:00
Andrey Antukh 3d176d5390 🐛 Restrict webhook creation/edit/delete to team members only (#11029)
* 🐛 Restrict webhook edit/delete to team members only

Remove the creator-id fallback from get-webhooks-permissions.
Previously, the webhook creator could always edit/delete their
webhook even after being removed from the team. Now can-edit
comes from team role only — removed users get :not-found.

Webhooks are NOT deleted on member removal; the team owns them
and team admins/owners manage them.

AI-assisted-by: mimo-v2.5-pro

* 🐛 Restrict webhook creation to team editors

Use team role check (check-edition-permissions!) for create-webhook
instead of the custom check that allowed any team member to create
webhooks via creator-id self-match override.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:42:49 +02:00
Andrey Antukh 0481408531 🐛 Add recursion depth limit to Fressian reader (#11020)
Bound read depth at 128 levels to prevent StackOverflowError from
crafted deeply-nested payloads. All recursive read handlers go
through read-object!, so a single depth check covers all paths.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:41:59 +02:00
Andrey Antukh 689d3a1be2 🐛 Add max-object-size guard to read-obj! in v1 parser (#11018)
Prevent unbounded memory allocation when a crafted binfile specifies
an excessively large object size. Apply the same 100 MiB limit that
read-stream! already enforces.

AI-assisted-by: mimo-v2.5
2026-08-05 17:40:58 +02:00
Andrey Antukh fb07273897 🐛 Validate library belongs to same team in link/unlink/sync handlers (#11016)
Add check-library-team-ownership! helper that verifies both the file
and library share the same team before creating or modifying library
relations. This prevents cross-team library injection where a user
with edit permissions on files in different teams could link them
across team boundaries.

Applied to link-file-to-library, unlink-file-from-library, and
update-file-library-sync-status handlers.

AI-assisted-by: mimo-v2.5
2026-08-05 17:40:04 +02:00
Andrey Antukh 9242556da6 🐛 Close import-binfile schema and remove file-id parameter (#10994)
Add :closed true to schema:import-binfile to reject unknown keys.
Remove file-id from handler destructuring, config binding, and audit
props to prevent specifying a target file on import.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 17:37:27 +02:00
Andrey Antukh 4f7bb94bb1 🐛 Add size limit and rate limiting to send-user-feedback (#10979) (#10990)
Prevent email bombing attacks on the send-user-feedback endpoint by
limiting the error-report field to 1MiB and adding climit rate limits:
by-profile (1 permit, queue 3) and global (4 permits), configured in
climit.edn. Make the schema public so it can be exercised by tests,
and add schema validation tests covering the new size limit.

AI-assisted-by: qwen3.7-plus
2026-08-05 17:35:59 +02:00
Andrey Antukh 49276886f3 🐛 Fix some issues with immutablejs incompatibility 2026-08-05 17:28:58 +02:00
Andrey Antukh a2968defbe 🐛 Add bounding box dimension limit to prevent export DoS (#11042)
Add max-export-dimension constant (100000 units) and validate in
calculate-dimensions. Reject exports when bounding box width, height,
or position exceeds the limit to prevent resource exhaustion in the
Chromium export pool.

AI-assisted-by: mimo-v2.5-pro
2026-08-05 14:18:14 +02:00
Marina López 6628f0a134 🐛 Adjust button icon visibility (#11070) 2026-08-05 11:30:13 +02:00
Andrey Antukh 636bc22cc4 Add Node.js E2E API tests for backend (#10787)
Add end-to-end HTTP tests under backend/test/e2e/ using Node.js built-in
test runner (node:test) and native fetch. Tests run through the devenv
nginx proxy on port 3450.

Test suites (19 tests total):
- auth-flow: demo profile creation, login, session cookies, access tokens
- export-binfile: file creation, export to asset URL via SSE
- asset-download: download with cookie/token auth, 401 without auth,
  S3 redirect behavior, full export-to-download flow

Key findings documented in tests:
- nginx @handle_redirect intercepts backend 307 and proxies to S3 directly,
  stripping the client Authorization header (bug does not reproduce in devenv)
- SSE end event uses ~#uri tagged format for URLs
- Unauthenticated RPC returns uuid/zero profile (not null)

AI-assisted-by: mimo-v2.5-pro

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-08-05 10:15:41 +02:00
Andrey Antukh aeedb96260 Add media-processor service for image and font processing (#10767)
*  Add media-processor service for image and font processing

Externalizes ImageMagick and FontForge subprocess invocations into a
separate Node.js HTTP service (media-processor/). Backend dispatches
via feature flag :use-remote-media-processing.

Key changes:
- media-processor module (TypeScript, Express 5, Sharp, FontForge/woff)
  - POST /api/image/info, /api/image/thumbnail, /api/font/generate
  - Resource limits: 128MP rejection, prlimit (512MB + 30s CPU)
  - Streaming multipart via SequenceInputStream
- app.media split into validation (leaf), local (shell impls), remote (HTTP)
- Schema enforcement: :upload and :input schemas in validation namespace
- Configurable timeout (PENPOT_MEDIA_PROCESSING_SERVICE_TIMEOUT)
- 78 tests across 4 files (image, font, middleware, config)
- FontForge path escaping for command injection prevention
- Parallel font variant conversions with Promise.all

AI-assisted-by: mimo-v2.5-pro

* 🐳 Revert docker-compose changes from media-processor commit

Remove docker-compose.yaml modifications that were part of the media-processor
service commit. The media-processor service definition, flags, and environment
variables are reverted to their previous state.

AI-assisted-by: qwen3.7-plus

* ⬆️ Update dependencies

* 🐛 Fix PR review issues in media-processor

- Font path bug: sfntToWoff and woff2ToSfnt now copy input to temp dir
  when input is a file path, ensuring output lands in expected location
- Error preservation: execCommand preserves killed/signal/code properties
  from child process errors for OOM detection
- Content-Length: service-multipart-request calculates and includes
  Content-Length header for streaming multipart requests

AI-assisted-by: qwen3.7-plus

* 🐛 Fix code review issues in media-processor

- Rename PENPOT_MEDIA_PROCESSOR_SECRET_KEY to PENPOT_MEDIA_PROCESSOR_SHARED_KEY
  in devenv to match backend config key
- Fix timeout middleware to destroy request AFTER response finishes,
  preventing truncated 504 responses
- Fix quality=0 parsing to preserve explicit zero (was silently overridden to 85)
- Replace require('fs') with proper ES module import in upload-storage.ts
- Refactor font conversion temp-dir boilerplate into withTempInput helper
- Document FontForge escaping limitations (single quotes only)
- Fix misleading comment in image.ts about sharp metadata decoding

AI-assisted-by: qwen3.7-plus

* 🐛 Fix code review issues in media-processor (round 2)

- Fix queue middleware to skip next() when response already ended,
  preventing orphaned work after timeout
- Fix hybrid storage to use disk when Content-Length is absent (chunked
  transfer), preventing unbounded memory allocation
- Add source image format validation in generateThumbnail to reject
  unsupported formats (TIFF, BMP, etc.) with 400 instead of 500
- Remove dead code in convertFont for unreachable woff→woff path
- Remove unused isEnabled() method from LokiLogTransport
- Fix sfntToWoff to use correct extension (.ttf/.otf) based on source type
- Extract queue middleware to separate file for testability
- Add comprehensive tests for queue middleware and upload storage

AI-assisted-by: qwen3.7-plus

* 🐛 Fix code review issues in media-processor (round 3)

- Fix disk-backed upload cleanup after successful requests by adding
  cleanup middleware that removes temp files on response finish/close
- Wrap sharp metadata/decoding errors as 400 validation errors instead
  of 500 internal errors
- Only apply flatten() for JPEG output to preserve alpha channel in
  PNG and WebP outputs

AI-assisted-by: qwen3.7-plus

*  Add comprehensive tests for media-processor

Phase 1 - Cleanup verification:
- Add cleanup middleware unit tests (6 tests)
- Add HTTP upload cleanup integration tests (5 tests)

Phase 2 - Error handling & alpha preservation:
- Add sharp error wrapping tests (4 tests)
- Add HTTP malformed image tests (2 tests)
- Add alpha preservation tests (3 tests)

Phase 3 - Edge cases:
- Add upload storage edge case tests (3 tests)
- Add queue middleware edge case tests (4 tests)

Phase 4 - Backend mock verification:
- Fix backend mocks to include :mtype field in image info responses
- Verify all error codes match actual service behavior

Total: 27 new tests added (160 tests passing)

AI-assisted-by: qwen3.7-plus

* 🐛 Fix code review issues in media-processor (round 4)

- Add Zod validation constraints for config values (int, positive, min)
- Fix auth middleware to compare Buffer byte lengths instead of string lengths
- Validate requested output dimensions in generateThumbnail (crop mode)
- Change queue middleware to release slot via callback in finally block
- Add comprehensive tests for all fixes

AI-assisted-by: qwen3.7-plus

* 🐛 Close HTTP response streams in backend media remote

- Wrap stream consumption in try/finally with .close() calls
- Add tests to verify stream closure for info, font-convert, and thumbnail

AI-assisted-by: qwen3.7-plus

* 🐛 Fix queue slot leak on upload failures

Make releaseQueue idempotent and attach fallback listener to release
slot when response finishes. This covers Multer errors that bypass
the route handler's finally block, preventing permanent queue stall.

AI-assisted-by: qwen3.7-plus

* 🐛 Cancel processing on timeout

Create AbortController in timeout middleware and abort signal when
timeout fires. Pass signal to Sharp and FontForge to cancel ongoing
processing and release resources when request is cancelled.

AI-assisted-by: qwen3.7-plus

* 🐛 Fix code review issues in media-processor (round 6)

- Error handler: check headersSent before writing response to prevent
  ERR_HTTP_HEADERS_SENT when timeout already sent 504
- Timeout config: increase default requestTimeout from 60s to 180s to
  match font processing timeout (120s) and backend request timeout
- Image processing: check abort signal before starting Sharp operations
  to cancel processing when timeout fires
- Queue lifecycle: remove res.on('close', release) fallback to hold
  queue slot until processing completes, preventing concurrency limit
  violation when client disconnects

AI-assisted-by: qwen3.7-plus

* 🐛 Close HTTP response stream in download-image

Wrap response body in with-open to ensure stream is closed after
writing to temp file, preventing HTTP connection leaks on repeated
URL imports.

AI-assisted-by: qwen3.7-plus

* 🐛 Close HTTP response stream on validation errors in download-image

Move with-open to wrap the entire validation and processing block,
ensuring the response body stream is closed even when validation fails
(non-2xx status, missing size, invalid media type). This prevents
HTTP connection leaks on repeated failed downloads.

Add test to verify stream closure on validation errors.

AI-assisted-by: qwen3.7-plus

* 🐛 Pass abort signal to Sharp toBuffer for timeout cancellation

Wrap Sharp's toBuffer() with Promise.race to check abort signal during
processing. This ensures large thumbnails stop processing when the
request times out, preventing wasted CPU/memory and queue capacity.

Add test to verify abort during toBuffer operation.

AI-assisted-by: qwen3.7-plus

* 🐛 Hold queue slot until Sharp completes and handle client disconnect

- Remove Promise.race from generateThumbnail — Sharp processing now
  completes fully before queue slot is released, preventing concurrency
  limit violations under timeout conditions
- Remove res.on("finish", release) fallback from queue middleware —
  error handler now explicitly calls releaseQueue in all error paths
- Add res.on("close") handler in timeout middleware to abort signal
  when client disconnects, ensuring processing stops early
- Add tests for client disconnect handling and queue slot lifecycle

AI-assisted-by: qwen3.7-plus

* 🐛 Address round 9 review findings

- Document Sharp 0.35.3 cancellation limitation in image.ts
- Add integration test for timeout cleanup with large images
- Fix font tools (sfntToWoff, woffToSfnt, woff2ToSfnt) to throw
  ProcessingError on resource limit kills instead of returning null
- Validate font signatures for same-format conversions to prevent
  arbitrary files from being persisted as valid fonts
- Fix concurrent mkdtemp race in upload-storage by using shared
  initialization promise

AI-assisted-by: qwen3.7-plus

* 🐛 Address round 10 review findings

- Add tmpdir assertion in font.ts to prevent path injection
- Preserve original error in queue middleware catch handler
- Change auth middleware response type from "internal" to "authorization"
- Add cleanup flag to prevent double cleanup in cleanup middleware
- Move quality clamping into parseQuality function for consistency
- Add integration tests for quality parameter clamping at route level
- Update existing tests to match new auth response type

AI-assisted-by: qwen3.7-plus

* 🐛 Address round 11 review findings

- Extract releaseSlot helper in error-handler to reduce duplication
- Remove redundant try/catch in font.ts withTempDir cleanup
- Improve font path validation error message for clarity
- Move path validation before try/catch to prevent swallowing
- Add debug logging for cleanup failures in cleanup middleware
- Inline TransportTargetSpec type alias in logger.ts
- Extract logging middleware to separate file for consistency
- Remove duplicate MIME validation in image thumbnail route
- Add test for font path validation (outside tmpdir rejection)
- Add tests for error handler queue release across all branches

AI-assisted-by: qwen3.7-plus

* 🐛 Remove Content-Length header from multipart requests

The JDK's HttpClient rejects Content-Length as a restricted header,
causing IllegalArgumentException when sending multipart requests to the
media-processor. Remove the explicit Content-Length header and let the
JDK use chunked transfer encoding. The media-processor will use disk
storage for all multipart requests (safe default behavior).

Remove unused size computations (file-size, header-bytes, footer-bytes,
total-size) that were only used for Content-Length.

Update test to verify Content-Length is not present in request headers.

AI-assisted-by: qwen3.7-plus

* 🐛 Fix pino ESM bundling for media-processor

Mark pino and its transports (pino-pretty, pino-loki) as external to
avoid bundling issues with worker thread modules that reference
__dirname (not available in ES modules).

AI-assisted-by: qwen3.7-plus
2026-08-05 09:41:48 +02:00
Andrey Antukh 3e59754a25 Add optional max-size param to blob decode functions
Accept an optional :max-size keyword argument in blob/decode and
blob/decode-str. When provided, the uncompressed size declared in the
blob header is validated before allocating memory, raising an error if
it exceeds the limit. Callers that do not pass :max-size are unaffected.

AI-assisted-by: deepseek-v4-pro
2026-08-05 08:38:55 +02:00
David Barragán Merino c16b7919f9 🐳 Remove the configuration of the admin-console from Nginx if it is not enabled 2026-08-04 20:33:31 +02:00
Andrey Antukh 83a3d099f6 🐛 Mock RPC and timer in composable test interpreter to fix network errors
The `SyncFromLibrary` op dispatches a real `sync-file` event that
schedules `rx/timer 3000` + an RPC call to
`update-file-library-sync-status`. In the headless test runner
(no backend), this produces a network error that leaks into test
output.

Wrap the `check` function in `mock/with-mocks` to mock `rp/cmd!`
(returning success) and `rx/timer` (firing instantly). This
eliminates the 3200ms grace period in `op-grace-ms` and prevents
the network error from appearing in test output.

AI-assisted-by: mimo-v2.5
2026-08-04 15:55:47 +00:00
Andrés Moya 8e713df5f0 🎉 Add repair functions for variant validation errors (#10768)
* 🎉 Add repair functions for variant validation errors

* 📚 Fix copyright notice
2026-08-04 16:53:36 +02:00
Filip SajdakandAndrey Antukh 3fba272848 🐛 Keep svg-raw children as uuids on binfile import (#10837)
Importing a .penpot file left every svg-raw subtree broken: the parent's
:shapes vector came back holding plain strings instead of uuids, so the
child ids no longer resolved against the page objects map. The next
persisted change touching that page then failed referential integrity
validation with :child-not-found, surfaced to the client as an HTTP 400
:referential-integrity error, which in practice bricks the file.

An svg-raw shape can be a container: importing an SVG builds a tree of
svg-raw shapes, and cfh/group-like-shape? explicitly treats an svg-raw
with children as group-like. But schema:svg-raw-attrs was an empty map.
Frame, group and bool all declare :shapes as a vector of uuid; svg-raw
did not, so the JSON decoder used by binfile had no type information for
those ids and left them as strings.

Declare :shapes on schema:svg-raw-attrs, optional because a leaf svg-raw
shape has no children, so the child ids decode back to uuids.
Closes #10496.

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-04 16:49:33 +02:00
Andrey Antukh 23ea2bbad6 📎 Update creating-commits serena workflow file 2026-08-04 15:36:54 +02:00
Andrey Antukh b507a6b667 ♻️ Convert commiter agent to create-commit skill
Replace the commiter subagent with a create-commit skill,
consistent with the create-pr and create-issue skill patterns.

- Remove .opencode/agents/commiter.md
- Add .opencode/skills/create-commit/SKILL.md
- Update implement-plan.md to use the skill instead of
  subagent delegation
- Document commit body line wrapping at 72 chars in
  creating-commits memory and skill

AI-assisted-by: mimo-v2.5
2026-08-04 13:08:22 +00:00
Andrey Antukh 3865e29b65 ⬆️ Update pnpm dependencies across all modules 2026-08-04 14:15:35 +02:00
Luis de Dios 43e05c38bf 🐛 Fix error when clicking 'Start' button to finish onboarding after creating a new account (#11058) 2026-08-04 12:44:25 +02:00
María Valderrama edbe9f8215 🐛 Fix nitrate related literals (#11047) 2026-08-04 11:55:01 +02:00
Andrey Antukh 319a2185c9 🐛 Fix crash on drag-and-drop of selected text in Draft.js editor (#10959)
After `Modifier.removeRange` modified the block map, the `targetRange` from
the drop event still referenced stale block keys from the pre-removal DOM
state, causing `TypeError: Cannot read properties of undefined`.

- Added a guarded `moveText` export in `frontend/packages/draft-js/index.js`
  that validates block keys exist before and after removal. Falls back
  gracefully when target range references stale keys.
- Added a `handle-drop` callback in
  `frontend/src/app/main/ui/workspace/shapes/text/editor.cljs` that returns
  "handled" for internal drag operations, preventing Draft.js from calling its
  default (crash-prone) handler.

AI-assisted-by: mimo-v2.5
2026-08-03 17:31:55 +02:00
Andrey Antukh 1136e5eda5 🐛 Restrict update-profile-props to documented keys only (#10992)
Close the profile props schema to reject undocumented keys and add a
denylist for system-managed props like :subscription that should not be
user-writable via RPC.

Changes:
- Add system-managed-props denylist (#{:subscription})
- Close schema:props with :closed true
- Add tests for subscription rejection and valid key acceptance

AI-assisted-by: qwen3.7-plus
2026-08-03 17:27:09 +02:00
Andrey Antukh 49119e0339 ♻️ Rename nitrate config to admin-console (#10929)
* ♻️ Rename nitrate config to admin-console

Rename user-facing configuration from 'nitrate' to 'admin-console':
- Feature flags: :nitrate -> :admin-console, :nitrate-bulk-create-profiles -> :admin-console-bulk-create-profiles
- Config keys: :nitrate-shared-key -> :admin-console-shared-key, :nitrate-backend-uri -> :admin-console-uri
- Shared-keys map entry: :nitrate -> :admin-console (setup.clj + main.clj)
- Env vars: PENPOT_NITRATE_SHARED_KEY -> PENPOT_ADMIN_CONSOLE_SHARED_KEY, PENPOT_NITRATE_BACKEND_URI removed (consolidated into PENPOT_ADMIN_CONSOLE_URI)
- Docker/nginx: PENPOT_NITRATE_URI -> PENPOT_ADMIN_CONSOLE_URI

Code namespaces, file paths, CSS classes, and i18n keys stay as-is.

AI-assisted-by: mimo-v2.5-pro

* ♻️ Rename initialize-user-in-nitrate-organization to initialize-user-in-organization

Part of the nitrate -> admin-console rename series. The function and all 9 references across 6 files have been renamed.

* ♻️ Rename :nitrate-bulk-create-profiles-not-allowed to :bulk-create-profiles-not-allowed

* ♻️ Inline nitrate-permissions into app.common.types.organization

- Delete app.common.types.nitrate-permissions and its test
- Move permission rules (allowed?, can-send-invitations?, etc.) into organization.cljc
- Harmonize all consumers to use alias cto for app.common.types.organization
- Update test runner and create organization_test.cljc
2026-08-03 17:23:55 +02:00
Andrey Antukh 79da4d274d 📎 Fix linter issues 2026-08-03 14:40:36 +02:00
Andrey Antukh 2f535c3f3f 🐛 Prevent unexpected exception in swatch* component 2026-08-03 14:25:56 +02:00
Andrey Antukh c320cecf15 📎 Add better prompt for review opencode command 2026-08-03 14:25:19 +02:00
Luis de Dios 141cf7f79f 🐛 Fix main menu does not keep alignment when left sidebar is expanded (#10986) 2026-08-03 10:18:06 +02:00
Luis de Dios 767f90282c 🐛 Fix shape size badge is displayed twice when user with viewer permissions selects a shape (#10985) 2026-08-03 10:16:01 +02:00
Andrey Antukh eacdca3c0a Merge remote-tracking branch 'origin/develop' into staging 2026-08-03 09:16:26 +02:00
Andrey Antukh b72536d312 Merge remote-tracking branch 'origin/staging' into develop 2026-08-03 09:16:09 +02:00
Andrey Antukh d835baefec Merge remote-tracking branch 'origin/staging' 2026-08-03 09:15:03 +02:00
Andrey Antukh f023c09018 Add testing skill with TDD workflow enforcement
Minimal skill referencing mem:testing for full guidance, covering
TDD Red-Green-Refactor cycle, Prove-It pattern for bug fixes,
and key testing principles.

AI-assisted-by: mimo-v2.5-pro
2026-08-01 11:32:14 +00:00
Andrey Antukh 32520e66e5 Allow stopping ws0 independently of other workspaces
Remove the guard in stop-devenv that refused to stop ws0 while any
ws1+ instance was running. Each workspace is now fully independent
and can be started/stopped in any order. Shared infra shuts down
only when no instances remain running.

Updated docs (devenv.md, agentic-devenv.md) and devenv memory to
reflect the new behavior.

AI-assisted-by: mimo-v2.5-pro
2026-08-01 11:29:42 +00:00
Andrey Antukh 5ad0ff752e 📎 Add minor changes on backend package.json 2026-08-01 11:20:31 +02:00
David Barragán Merino 3fc5360df5 🔧 Optimize the bundle and docker images build process 2026-07-31 20:39:51 +02:00
Andrey Antukh b907bc4cd3 📚 Add hard rule for test output handling to serena memories
Never pipe test output directly to filters (head, tail, grep).
Always redirect to a file first to prevent hiding test failures.

AI-assisted-by: mimo-v2.5
2026-07-31 19:45:06 +02:00
Andrey Antukh 73519778ae 📎 Update serena memories 2026-07-31 19:31:45 +02:00
David Barragán Merino 97a833f1e7 🐳 Migrate penpot images to DHI (#10734)
Move Dockerfile.frontend, .backend, .exporter, .mcp and .storybook
under docker/images/ from ubuntu:26.04 / nginx-unprivileged / a
manual Node tarball install to Docker Hardened Images (Debian 13,
or Alpine for storybook). storybook and mcp get a true non-dev
runtime; frontend, backend and exporter keep the -dev tag as their
final image, since each needs a shell and/or package manager at
container runtime (nginx templating, fontforge/python3, and a
headless-browser stack, respectively).
2026-07-31 19:27:15 +02:00
Andrey Antukh e741313add Merge remote-tracking branch 'origin/staging' into develop 2026-07-31 19:21:07 +02:00
Andrey Antukh 896aef8c1b 📎 Update imagemagick version on manage.sh 2026-07-31 19:04:33 +02:00
David Barragán Merino 328fa0559a 🐳 Migrate imagemagick and devenv images to DHI
Move docker/imagemagick/Dockerfile and docker/devenv/Dockerfile from
ubuntu:26.04 to Docker Hardened Images (Debian 13 / trixie).
imagemagick gets a true non-dev runtime with its shared libraries
vendored via ldd; devenv keeps the -dev tag as its final image since
it's an interactive development container, not a production
artifact.
2026-07-31 19:04:33 +02:00
Anonymous 863671135d 🌐 Add translations for: French (Canada)
Currently translated at 95.7% (2329 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-07-31 19:03:01 +02:00
Alexis Morin 657b5bd94e 🌐 Add translations for: French (Canada)
Currently translated at 95.7% (2329 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-07-31 19:03:00 +02:00
Anonymous 5ea3a2efb8 🌐 Add translations for: Hindi
Currently translated at 81.1% (1974 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-07-31 19:02:58 +02:00
VKing9 94cfd6e60d 🌐 Add translations for: Hindi
Currently translated at 81.1% (1974 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-07-31 19:02:58 +02:00
Anonymous 600c573ccd 🌐 Add translations for: Swedish
Currently translated at 96.4% (2345 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-07-31 19:02:56 +02:00
AntonPalmqvist 4c388f6d8b 🌐 Add translations for: Swedish
Currently translated at 96.4% (2345 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-07-31 19:02:55 +02:00
Anonymous 4d07510581 🌐 Add translations for: Dutch
Currently translated at 94.4% (2297 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-07-31 19:02:53 +02:00
Stephan Paternotte 5e8a2b0a03 🌐 Add translations for: Dutch
Currently translated at 94.4% (2297 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-07-31 19:02:53 +02:00
Anonymous 588222d409 🌐 Add translations for: Latvian
Currently translated at 75.7% (1842 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-07-31 19:02:51 +02:00
Anonymous 0139c0f0e3 🌐 Add translations for: Korean
Currently translated at 81.5% (1984 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-07-31 19:02:49 +02:00
Denys Kisil 1b69d8c6a2 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 83.5% (2032 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-07-31 19:02:48 +02:00
Anonymous d7b55a245a 🌐 Add translations for: Italian
Currently translated at 86.5% (2105 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-07-31 19:02:46 +02:00
Anonymous fdeff910fe 🌐 Add translations for: Persian
Currently translated at 31.4% (765 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2026-07-31 19:02:44 +02:00
Anonymous 3fa21180fd 🌐 Add translations for: Hebrew
Currently translated at 83.0% (2020 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-07-31 19:02:42 +02:00
Yaron Shahrabani ecaf63b204 🌐 Add translations for: Hebrew
Currently translated at 83.0% (2020 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-07-31 19:02:42 +02:00
Anonymous 1074cb376d 🌐 Add translations for: Romanian
Currently translated at 78.3% (1906 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2026-07-31 19:02:40 +02:00
Anonymous 0aff047301 🌐 Add translations for: German
Currently translated at 79.8% (1942 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-07-31 19:02:38 +02:00
Anonymous 02b0f18a83 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 56.4% (1372 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2026-07-31 19:02:36 +02:00
DoubleCat 13b2954cbf 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 94.4% (2298 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-07-31 19:02:35 +02:00
Anonymous 0703a3fcc1 🌐 Add translations for: Turkish
Currently translated at 96.4% (2345 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-07-31 19:02:33 +02:00
Oğuz Ersen b294aecb7c 🌐 Add translations for: Turkish
Currently translated at 96.4% (2345 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-07-31 19:02:32 +02:00
Anonymous 620c26b304 🌐 Add translations for: Russian
Currently translated at 68.4% (1664 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-07-31 19:02:30 +02:00
Anonymous 1ac000bc27 🌐 Add translations for: French
Currently translated at 82.1% (1999 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-07-31 19:02:28 +02:00
Ingrid Pigueron 9042bd5129 🌐 Add translations for: French
Currently translated at 82.1% (1999 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-07-31 19:02:27 +02:00
Anonymous 905c230e17 🌐 Add translations for: Spanish
Currently translated at 95.4% (2321 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2026-07-31 19:02:25 +02:00
Hosted Weblate 552cda9aae 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2026-07-31 18:57:18 +02:00
Andrey Antukh 89029060e0 Merge remote-tracking branch 'weblate/develop' into develop 2026-07-31 18:56:46 +02:00
Andrey Antukh 07efd8fb99 ⬆️ Update dependencies (#10982)
* ⬆️ Update root pnpm depdendencies

* ⬆️ Update common pnpm dependencies

* ⬆️ Upgrade docs pnpm dependencies

* ⬆️ Upgrade pnpm deps on library

* ⬆️ Upgrade plugins pnpm dependencies

* ⬆️ Update pnpm dependencies on mcp

* ⬆️ Update pnpm dependencies on frontend

* ⬆️ Update exporter pnpm dependencies

* ⬆️ Update jvm/clojure deps

* ⬆️ Update docker dependencies (jvm and node)

* 📎 Add minor fixes
2026-07-31 18:53:05 +02:00
David Barragán Merino 31158c319a 🔧 Optimize the Docker image build process (avoid rebuilds, make better use of the cache) 2026-07-31 16:57:04 +02:00
Andrey Antukh ab1c70115b 🐛 Prevent MCP tokens from being used as access tokens (#10962)
MCP tokens now use a separate JWT issuer claim (`urn:penpot:mcp-token`) instead of `access-token`, preventing them from being validated as API access tokens.

Fixes #10960

AI-assisted-by: qwen3.7-plus
2026-07-31 13:19:45 +02:00
Eva Marco 9bb5861322 🐛 Fix undo delete pages change order (#10969) 2026-07-31 13:09:18 +02:00
Andrey Antukh d04cbf175e 🐛 Fix nil dereference crash during flex layout drag operations (#10845)
Production crash where @(get bounds id) threw
"No protocol method IDeref.-deref defined for type null"
when a shape ID had no corresponding entry in the bounds map
during layout calculations.

Added defensive nil guards (when-let / when) to all unprotected
bounds dereference sites:

- flex_layout/bounds.cljc: layout-content-points (parent + child)
  and layout-content-bounds
- grid_layout/bounds.cljc: layout-content-points and
  layout-content-bounds
- min_size_layout.cljc: child-min-width grid branch (3 sites) and
  child-min-height grid branch

Added 7 new tests in geom_bounds_layout_nil_test.cljc covering all
nil-bounds edge cases for flex, grid, and min-size layout paths.
Registered in runner.cljc.

Closes #10843

AI-assisted-by: qwen3.7-plus
2026-07-31 12:51:35 +02:00
Eva Marco 36553a7c6f 🐛 Fix reset cancel shortcut (#10980) 2026-07-31 12:51:01 +02:00
Luis de Dios ff32f104b9 🐛 Fix main menu is covered by the toolbar (#10926)
* 🐛 Fix main menu is covered by the toolbar

* ♻️ Refactor SCSS

* 🐛 Fix adjust z-index of workspace context menu

* ♻️ Refactor SCSS

* 🐛 Fix adjust z-index of tokens context menu

* ♻️ Refactor SCSS

* 🐛 Fix adjust z-index of old context menu

* 📎 PR improvements
2026-07-31 12:48:59 +02:00
María Valderrama 1744d07731 Allow nitrate onboarding to be handled by admin-console (#10936)
*  Allow nitrate onboarding to be handled by admin-console

* 📎 Code review
2026-07-31 12:24:30 +02:00
Andrey Antukh 764b62906b 🐛 Handle unrecognized JSON escape sequences as malformed-json (#10808)
* 📎 Update serena documentation about creating-prs workflow

* 🐛 Handle unrecognized JSON escape sequences as malformed-json

When clojure.data.json's read-escaped-char encounters an unrecognized
escape sequence (e.g. a backslash followed by '}', or other case
fall-throughs in the parser) in a JSON request body, it throws a bare
IllegalArgumentException. Previously this fell through to the generic
RuntimeException branch in wrap-parse-request's handle-error, which
unwrapped and recurred without matching, eventually reaching the
internal-error handler and producing HTTP 500 + an error report — even
though the root cause was malformed client input, not a server bug.

The fix converts any IllegalArgumentException raised in the JSON parse
path into a `:validation`/`:malformed-json` error by raising a new
ex-info (which is caught by the top-level error handler in
`app.http/router-handler`). The result is an HTTP 400 response with a
descriptive hint, and no error report is generated. This addresses
~10% of all error reports received.

The new IAE branch is placed before the RuntimeException branch in
the cond (since IllegalArgumentException IS-A RuntimeException) and
uses the throw-style (ex/raise) to match the existing
RequestTooBigException / EOFException branches. A comment above the
handle-error cond documents why raising is intentional and is caught
by the top-level app.http error handler, not by the per-route
wrap-errors middleware.

Test suite changes:

- Extend the existing `DummyRequest` defrecord in
  `http_middleware_test.clj` from 2 fields to 12 fields, implementing
  every IRequest method, and add a private `make-dummy-request`
  constructor that accepts an options map with every key optional and
  sensible `:or` defaults. Future fields added to DummyRequest won't
  break existing call sites as long as the `:or` defaults are kept in
  sync.

- Remove the now-redundant `JsonRequest` defrecord and migrate all 11
  `->DummyRequest` call sites to `make-dummy-request`.

- Add 6 new deftest cases:
  - parse-request-illegal-argument-exception: malformed JSON body
    (containing `\}`) is converted to `:malformed-json`.
  - parse-request-request-too-big-exception: RequestTooBigException
    is converted to `:request-body-too-large`.
  - parse-request-eof-exception: java.io.EOFException is converted
    to `:malformed-json`.
  - parse-request-runtime-exception-with-cause: a wrapped
    RuntimeException recurses on ex-cause and dispatches to the
    matching specific branch.
  - parse-request-runtime-exception-without-cause: a bare
    RuntimeException falls through to errors/handle, returning 500
    with :type :server-error :code :unexpected.
  - parse-request-non-runtime-throwable: java.io.IOException (a
    non-RuntimeException Throwable) is handled by the dedicated
    handle-exception method, returning 500 with :code :io-exception.

Together, the new tests cover all 6 branches of wrap-parse-request's
handle-error cond.

Refs #10804.

AI-assisted-by: minimax-m3
2026-07-31 12:06:19 +02:00
Marina López 94f51afb20 ♻️ Normalize organization naming across Penpot (#10977) 2026-07-31 12:04:10 +02:00
Elena Torró a2231a8bf9 🐛 Fix text align shown as left in v3 editor (#10973) 2026-07-31 11:48:31 +02:00
Luis de Dios f5b17a5c75 🐛 Fix toolbar keyboard navigation (#10939)
* 🐛 Fix flyouts are not expanded using just the keyboard

* 🐛 Fix escape key does not close the flyouts
2026-07-31 11:25:46 +02:00
Luis de Dios e952d9c70a 🐛 Fix draw a line/arrow with a single click (#10974) 2026-07-31 11:25:20 +02:00
Luis de Dios 3fc1e6aadd 🐛 Fix image swatches displays a wrong format in list view (#10975) 2026-07-31 11:24:55 +02:00
Filip SajdakandAndrey Antukh 0b5db57def 🐛 Highlight first search result in font picker and fix Enter confirm (#10450)
When the user types a search term that filters out the currently-active
font, the picker showed no selection at all and Enter would close without
applying any font. Fix:

- Compute effective-selected (render-only, no state mutation) as the
  first filtered font when the current font is absent from the results.
  The row renderer and recent-fonts list use this for the tick mark, so
  the top match is visually pre-selected while the user types.
- Enter key applies first-result (via on-select then on-close) when the
  current selection is not in the filtered list; otherwise closes as
  before. No font is live-applied on every keystroke.
- on-key-down deps extended to on-select/on-close; effect deps include
  on-key-down so the global listener is always current.

Avoids the live-apply side-effect that caused the revert of #9512.
Fixes #3204.

Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-31 10:46:53 +02:00
Belén Albeza 7fa6631005 🐛 Fix collapsed fills section on text shapes (#10972) 2026-07-31 10:29:26 +02:00
Elena Torró 9b0be1c750 🐛 Fix crash when editing justified text in wasm editor (#10945) 2026-07-31 10:20:42 +02:00
Eva Marco fe85a5717e ♻️ Fixes on shortcuts (#10906)
* 🎉 Improve reset to default flow

* 🎉 Add diff modal

* 🐛 Fix measurement shortcuts

* 🎉 Separate open-sections for each tab

* 🐛 Align button link icon

* 🎉 Reset only works after press save

* 🐛 Fix little things

* 🎉 Make the import export row to be fixed

* ♻️ Fix CI

* 🐛 Cancel shortcut is not appearing on disabled tab

* 🐛 Fix tests

* 🐛 Fix loop on personalized

* 🐛 Fix show measurements shortcut
2026-07-30 16:56:10 +02:00
Pablo Alba 370e91c3e3 🐛 Fix bad error callback on nitrate checkout (#10886) 2026-07-30 16:50:47 +02:00
Marina López 31ccfa546f Add nitrate audit events 2026-07-30 15:32:45 +02:00
Belén Albeza 91d4b16171 🐛 Fix selrect not being recalculated after pasting properties (#10963) 2026-07-30 15:01:50 +02:00
Eva Marco c8867f8c3a 🐛 Fix registration mail color (#10965) 2026-07-30 14:51:32 +02:00
Dominik Jain 30943f1074 Make MCP tool call timeout configurable, raising default
The timeout for tool calls (which is trictly relevant for plugin tasks only)
is now configurable via env. var PENPOT_MCP_TOOL_TIMEOUT_S.

The default was raised from 30 to 120, because 30 seconds was not enough for
some calls, especially in larger Penpot files. #10953
2026-07-30 14:21:42 +02:00
Dominik Jain b4659df5b2 🐛 Preserve established plugin connection when rejecting a duplicate #10961
In multi-user mode, rejecting a second plugin WebSocket connection for an
already-registered user token performed the full removeConnection cleanup
for the newcomer. Since the token-keyed cleanup is keyed by token rather
than by socket, this deleted the clientsByToken entry and the Redis
request-channel subscription of the established, healthy connection. That
connection then remained open and heartbeating but was unroutable, so every
subsequent MCP tool call for the user failed although a valid plugin
connection existed.

removeConnection now performs the token-keyed cleanup only if the removed
connection actually owns the token registration, so rejecting a duplicate
releases only the resources the newcomer itself registered.

AI-assisted-by: claude-fable-5
2026-07-30 14:21:42 +02:00
Dominik Jain 1ae9334064 🐛 Fail fast on Redis task dispatch when no instance is connected #10958
In multi-user mode, plugin task requests are published to a Redis channel
keyed by user token. When no MCP server instance held a plugin connection
for that token (e.g. after the user navigated away from the workspace),
the publish reached zero subscribers and the request was silently dropped,
so every tool call stalled until the 30-second task timeout instead of
failing with a meaningful error.

RedisBridge.sendTaskRequest now returns the PUBLISH receiver count and
releases its response-channel subscription when the request reached no
receiver (or publishing failed), since no response can arrive. PluginBridge
uses the count to reject the pending task immediately with the multi-user
connection error message; publish failures likewise reject the task instead
of surfacing as an unhandled rejection followed by a timeout. The pending-
task settlement logic shared with the timeout handler is extracted into a
rejectPendingTask helper.

AI-assisted-by: claude-fable-5
2026-07-30 14:21:42 +02:00
014ec34249 🐛 Fix system crash on grid element deletion of a component (#10956)
* 🐛 Add regression test: main-side component edits break copy swap slots

Reproduces the :missing-slot referential-integrity failure ("Shape has been
swapped, should have swap slot") that crashes files with component copies.

Root cause: reordering or deleting a nested sub-head IN THE MAIN of a component,
while copies exist, does not propagate swap slots to the copies. find-near-match
matches a copy's sub-heads to the main's children by POSITION, so once the main's
order changes the copies' shape-refs no longer match their position and, lacking a
swap slot, fail referential-integrity validation.

- Copy-side edits are handled correctly (characterization tests, pass today).
- The two main-side tests fail today with :missing-slot and go green once the
  sync assigns swap slots to copies on a main reorder/delete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🐛 Fix integrity crashes from copy/main child-order divergence

Copy sub-heads were matched to their main's children purely by position
(find-near-match), while several code paths reorder or mutilate one side
only. Any of them made file validation fail with :missing-slot ("Shape
has been swapped, should have swap slot"), crashing the workspace on the
next validated commit, or persisting a corrupt file whose later edits
crash. Reproduced live: deleting a sub-head inside a copy (which only
hides it) and then reflowing the copy's grid moved the hidden (cell-less)
child to the front of :shapes, knocking every sibling out of its
positional slot.

Four fixes, one per divergence path:

- validate/check-required-swap-slot: a sub-head whose shape-ref is still
  a child of the near main parent is a REORDER (the component sync
  realigns it), not a swap; a swap slot is required only when the ref
  points outside the near main parent (a real swap). This matches how
  the sync engine itself pairs children (by shape-ref, not by position).
  comp-processors/fix-missing-swap-slots (migration 0019) is aligned:
  adding slots to merely-reordered sub-heads would freeze them out of
  normal synchronization.

- changes/:reorder-children now refuses to alter the child structure of
  component copies unless allow-altering-copies is set, mirroring the
  is-valid-move? rule of :mov-objects; that structure is owned by the
  component sync engine. Grid reflows emitted this change type with no
  guard. pcb/reorder-grid-children also skips copy grids producer-side.

- layout/reorder-grid-children keeps children that participate in no
  cell (hidden or absolute positioned) at their original index instead
  of lumping them at the front: moving them gratuitously changed their
  z-order and, in copies, broke the positional matching. Note :shapes
  stays reversed relative to the sorted cell order for in-cell children.

- logic/generate-delete-shapes: deleting shapes from inside a main
  (without deleting the main root, whose copies keep working against the
  deleted component) now also deletes the copy shapes that reference
  them, transitively (copies of copies) and across all pages of the
  file, so no dangling shape-refs remain. Skipped for
  allow-altering-copies flows (component swap replaces the shape and the
  sync reconciles copies via swap slots). Cross-page removals build
  redo/undo changes against that page's objects directly, since the
  changes-builder mounts only the current page; their undo mov-objects
  carry allow-altering-copies so restoring inside a copy is not rejected
  by the new guard.

The regression tests assert the fixed semantics: main-side reorders and
deletes keep copies valid, the previously crashing full chain (unvalidated
main reorder + later copy edit) stays healthy, :reorder-children cannot
scramble copies, and reorder-grid-children keeps cell-less children in
place. The namespace is now also registered in the JS test runner.

AI-assisted-by: Claude Opus 4.8 (1M context)

*  Extend composable slot cases to the grid-reflow crash sweep

Case D (CopySubheadDeletePreservesSlots) now sweeps which copy sub-head
is deleted (first or last) and whether the copy root is resized
afterwards, forcing a grid reflow: the reflow used to move the hidden
(cell-less) child to the front of the copy's children, shifting every
sibling out of its positional slot. Deleting the FIRST sub-head masked
the bug (moving it to the front is a no-op), which is why the case
passed before this sweep. The foundation layout is grid accordingly.

Case E (MainReorderKeepsCopySlots) no longer hangs the app now that a
main-side reorder leaves a valid file, so its warning docstring is
replaced: it runs as a routine test (verified headless, passing) and is
safe in a "run all".

SlotIntegrity's doc is updated to the new validator semantics (a slot is
required only for real swaps, not reorders); the positional alignment it
asserts remains the correct, stronger steady-state invariant for these
cases. The lockfile change materializes the playwright devDependency
already declared in package.json.

AI-assisted-by: Claude Opus 4.8 (1M context)

* 📚 Record copy/main order-divergence invariants in memories

Swap-slot semantics (membership, not positional; slots only for real
swaps), the copy-structure guards on :mov-objects/:reorder-children, the
grid reorder stability for cell-less children, and the main-side delete
propagation in generate-delete-shapes.

AI-assisted-by: Claude Opus 4.8 (1M context)

* 🐛 Add adjustements to the code

---------

Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 13:25:03 +02:00
Eva MarcoandAndrey Antukh e705b6e12c 🎉 Add components library (#10675)
* 🎉 Install react-aria-components

* 🎉 Create modal component in TS using react-aria-component

* 🎉 Create modal ds component

* 🎉 Separate header content and footer components

* 🐛 Remove mf/html macros when not needed

* 🎉 Solve little problems

* ♻️ Format files

* ♻️ Remove ModalCloseBtn

* 🐛 Fix CI

* ♻️ Remove unused files

* 🎉 Make close button not dependant on the modal header

* 🎉 Add footer with two slots

* 🎉 Improvements on modal

* 🐛 Fix package imports and remove login example code

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-30 13:05:28 +02:00
972353eccb 🐛 Fix layout padding persisted as string after invalid input (#10758)
Combined fixes from PR #10656 (numeric-input redesign) and PR #10696
(global finite? guard) for issue #10638.

- Add (number? v) guard to cljs mth/finite? so strings are rejected
- Redesign numeric-input last-value* to store number, not formatted string
- Invalid-input fallback restores display without emitting on-change
- Esc now fully discards typed text (resets raw-value* + dirty flag)
- Token dedup by name instead of resolved value
- Defense-in-depth: d/parse-double at 4 padding/gap handlers
- Math tests (common), unit tests, Storybook play tests, Playwright E2E

AI-assisted-by: deepseek-v4-flash

Co-authored-by: Akshit Nassa <nassaakshit@gmail.com>
Co-authored-by: Ulises Millán <ulises.millanguerrero@gmail.com>
2026-07-30 12:51:44 +02:00
Andrey Antukh ef593514f2 🐛 Add nil guards on viewport-node in pixel overlay component (#10812)
* 🐛 Add nil guards on viewport-node in pixel overlay component

Add nil checks for viewport-node in process-pointer-move, viewport->canvas-coords, process-pointer-move-wasm, pick-color-at-wasm, and handle-draw-picker-canvas.

Fixes a crash ("can't access property 'getBoundingClientRect', ... is null")
when the viewport DOM node is unmounted while the color picker eyedropper
is active and pointer move events are still firing.

Fixes #10811

AI-assisted-by: mimo-v2.5

* 🐛 Remove unused app.common.pprint require from errors.cljs

Fixes clj-kondo warning: namespace app.common.pprint is required but never used.

AI-assisted-by: mimo-v2.5
2026-07-30 12:50:46 +02:00
Andrey Antukh 040080749b 🐛 Fix shape export failures when export name is nil or empty (#10852)
Use cuerdas blank-name handling directly when normalizing frontend export
payloads. Replace nil or blank export names with the object-id string in
request-simple-export, request-multiple-export, clipboard export, and plugin
direct export payloads so that the backend always receives a valid name. Add
focused frontend tests for nil/blank name normalization and normalized request
params.

AI-assisted-by: nex-n2-pro
2026-07-30 12:49:19 +02:00
Andrey Antukh fadb3124a0 🐛 Clamp gradient stop offsets to valid range (#10881)
* 🐛 Clamp gradient stop offsets to valid [0, 1] range

Fixed a bug where gradient stop offsets outside the valid [0, 1] range were being sent to the server, causing schema validation errors ('invalid shape found').

Changes:
- Viewport gradient handler: clamp offset in points-on-pointer-down before creating new stops
- Colorpicker gradient preview: clamp offset in handle-preview-down before adding stops
- Data layer: clamp offset parameter in update-colorpicker-add-stop and all stop offsets in update-colorpicker-stops; added app.common.math require

All clamping follows the existing pattern used in handle-marker-pointer-move.

AI-assisted-by: deepseek-v4-flash

* 💄 Fix formatting in gradient handlers

Fix cljfmt formatting issues in gradient handler functions.

AI-assisted-by: qwen3.7-plus
2026-07-30 12:49:00 +02:00
Pablo Alba 3eb50ef50e 🐛 Fix nitrate ignores sso token expiration (#10921) 2026-07-30 12:40:47 +02:00
Elena Torró a722a33503 🐛 Fix gradients and text selection (#10941)
* 🐛 Add missing text fills

*  Keep text leaf selected while editing
2026-07-30 12:11:53 +02:00
Pablo Alba 03f9393200 🐛 Fix nitrate sso doesn't notify workspace on activation (#10955) 2026-07-30 11:56:06 +02:00
Belén Albeza be5d9f9e9c 🐛 Fix invalid shape due to missing fonts (#10942)
* 🐛 Fix setting a font-related properties to nil

*  Repair already-corrupted text nodes

* 🐛 Fix deleted fonts appearing in Recents and as the auto-selected font for new text shapes
2026-07-30 11:18:19 +02:00
Luis de Dios 2cb8813862 Remember expanded/collapsed state of token sets in the colorpicker (#10864) 2026-07-30 10:47:11 +02:00
Luis de Dios 69649cd608 🐛 Fix tick icons are not aligned in font selector (#10774) 2026-07-30 10:00:52 +02:00
Luis de Dios e5d588ca54 🐛 Fix inputs broken when there are tokens on them (#10905) 2026-07-30 09:59:11 +02:00
Alejandro Alonso 485cb8e5ec Merge remote-tracking branch 'origin/staging' into develop 2026-07-30 09:04:31 +02:00
Andrey Antukh 25618febcd Merge remote-tracking branch 'origin/main' into staging 2026-07-30 08:46:52 +02:00
Andrey Antukh 5e5465a0fe 🐛 Fix audit event validation for error reports with string profile-id (#10898)
The audit event validation was failing when processing error reports that
contain string profile-id values. The error report storage converts
profile-id to string format, but the audit schema expects a UUID.

Changes:
- Modified prepare-rpc-event to convert string profile-id to UUID using
  uuid/parse* (exception-safe parsing)
- Updated access token middleware to set ::id and ::type on request so
  audit context includes token identification
- Added tests for profile-id conversion and token context population

Closes #10897

AI-assisted-by: qwen3.7-plus
2026-07-30 07:32:02 +02:00
Andrey Antukh 0ed151d2ca Merge remote-tracking branch 'origin/staging' into develop 2026-07-29 19:55:33 +02:00
Andrey Antukh adda7e6645 📚 Update testing serena memories 2026-07-29 19:55:12 +02:00
Belén Albeza 3e7fb07931 🐛 Fix undo crash after deleting a word (#10862) 2026-07-29 15:53:41 +02:00
Alejandro AlonsoandCursor c8d1f5b397 🐛 Guard finalize-view-interaction! against spurious pointerup events (#10917)
Every pointerup unconditionally fires finish-panning and finish-zooming,
which call finalize-view-interaction!. This triggered internal-render
(and reset_canvas) on plain clicks — causing a visible white flash on
large viewports or weak GPUs.

Add a guard so finalize-view-interaction! only runs when a view
interaction (pan/zoom) is actually active.

Fixes #10915

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 11:43:29 +02:00
Marina López 056cd3d379 🐛 Fix SSO review 2026-07-29 11:26:16 +02:00
Alejandro Alonso 4cd34b22e2 🐛 Fix wasm context restore guards (#10824)
* 🐛 Prevent WASM panics during WebGL context restore

Delay context-restored until reload finishes and no-op app
mutations while reloading so resize/modifiers cannot panic
mid-teardown.

* 🐛 Fall back to CLJS bool content when WASM is not ready

Returning nil from calculate-bool would persist empty path content
during context loss/reload; use path/calc-bool-content instead.
2026-07-29 10:06:52 +02:00
Eva Marco e14e7bb616 ♻️ Refactor and remove deprecated css (#10698)
* ♻️ Fix Alert component

* ♻️ Fix auth files

* ♻️ Fix comment files

* ♻️ Fix confirm files

* ♻️ Fix dashboard files

* ♻️ Fix settings files

* ♻️ Fix static files

* ♻️ Fix color bullet file

* ♻️ Fix color workspace file

* ♻️ Fix color common file

* ♻️ Fix color recovery request file

* ♻️ Fix color recovery file

* ♻️ Fix disabled login buttons

* 🐛 Fix button link page

* ♻️ Fix format
2026-07-29 09:54:32 +02:00
David Barragán Merino 66201ac437 🐳 Add --tag/--push flags to docker image build commands (#10732)
Local-only builds by default for build-devenv and
build-imagemagick-docker-image; --push is now required to build
multi-platform and push to the registry. All release-image build
commands (frontend, backend, exporter, mcp, storybook) now accept
--tag to override the image tag. Adds a shared DEVENV_TAG variable
threaded through pull-devenv, the production build function and
docker-compose.main.yml so a custom devenv tag can be used
end-to-end.
2026-07-29 08:47:08 +02:00
Pablo Alba 70675919c7 Add link to download a nitrate activation code request (#10900) 2026-07-29 08:29:05 +02:00
David Barragán Merino 9f92206a1c 🔧 Synchronise Admin Consoel's Docker image build process 2026-07-28 18:43:28 +02:00
Belén Albeza 704cd40182 🐛 Fix caret not mimicking text color (#10866) 2026-07-28 16:25:47 +02:00
Alonso Torres cb21f6401a Add waitForLayoutUpdate plugin method (#9898)
*  Add waitForLayoutUpdate plugin method

*  Refactor internal wait for tasks
2026-07-28 14:44:30 +02:00
Luis de Dios 0999bc31b2 🐛 Fix disabled/mixed-value state when per-side stroke is expanded/collapsed (#10874) 2026-07-28 13:28:16 +02:00
Andrey Antukh 343865cf27 🐛 Fix issues with ci script 2026-07-28 13:08:29 +02:00
Andrey Antukh e9603f00b0 Merge remote-tracking branch 'origin/staging' into develop 2026-07-28 12:50:41 +02:00
Andrey Antukh bbc7e9bee9 Add a script for run ci-like tasks 2026-07-28 12:47:33 +02:00
Andrey Antukh 65111195a9 📎 Fix missing require 2026-07-28 12:39:31 +02:00
Andrey Antukh 6bbd3fa364 📎 Fix syntax error introduced in prev merge 2026-07-28 12:18:06 +02:00
Elena Torró d4d22ff1a4 🔧 Prepare wasm render for headless exporter (#10795)
* 🔧 Support background blur on PDF render for strokes and text shapes

*  Add LRU eviction to the wasm image store

*  Add RasterFormat to encode png, jpeg and webp from wasm

* 🐛 Fix client-side wasm export encoding jpeg and webp as png
2026-07-28 11:54:22 +02:00
Marina López 0c02121d5d 🐛 Fix alignment from organization and team selector 2026-07-28 11:16:48 +02:00
Marina López 7674229a5b 🐛 Fix bullets position for subscription list 2026-07-28 11:16:48 +02:00
Dr. Dominik Jain af00860c13 🐛 Report the effective UI theme to plugins (#10677)
Penpot itself and plugins used different default theme selection
strategies when no theme is explicitly selected in Penpot by the user:

- Penpot itself defaults to dark.
- Plugins defaulted to whatever is configured in the user's
  system/browser, which could be light.

So with no explicit theme chosen in the profile — the common state —
every plugin could be told the theme was light while Penpot itself
showed dark, both on open (penpot.theme) and on themechange. For every
explicit choice (light, dark, system, and the legacy default) the two
resolutions already agreed; only the unset case diverged.

Extract the app's resolution into app.util.theme/resolve-theme (system
follows the system theme; default and unset mean dark) as the single
source of truth, and use it in the app's own use-initialize as well as
in the plugin runtime's getTheme and themechange handling. The
themechange handler's now-dead default-to-dark remapping is removed.

Fixes #10676

AI-assisted-by: claude-fable-5
2026-07-28 11:09:17 +02:00
Andrey Antukh 4b994d20aa 🐛 Fix leaked focus timers in dashboard sidebar navigation (#10715)
* 🐛 Fix leaked deferred DOM ops on dashboard navigation and template clone

The React reconciliation "removeChild" error surfaced during rapid
dashboard navigation because several effects scheduled deferred DOM
operations (focus, CSS positioning) without returning a cleanup that
cancelled them. When the component unmounted before the callback
fired, it ran against stale DOM and desynchronized React fiber tree
from the actual DOM.

- context_menu_a11y.cljs: replace tm/schedule-on-idle (30s idle
  window) with tm/schedule (setTimeout 0) and return a rx/dispose!
  cleanup.
- dropdown.cljs: capture the tm/schedule handle and dispose it in
  the effect cleanup.
- tooltip.cljs: capture the ts/raf handle and cancel it on cleanup.

AI-assisted-by: opencode-go/mimo-v2.5-pro

* 🐛 Fix leaked focus timers in dashboard sidebar navigation

Six sidebar navigation handlers scheduled setTimeout callbacks to mutate
tabindex/focus on React-managed title elements without cancelling prior
pending callbacks. During rapid keyboard navigation (Projects→Fonts→Libs→Drafts)
the stale callbacks fired against unmounted DOM, desyncing React fiber tree
and triggering "removeChild" NotFoundError.

- sidebar-project*: cancel prior timer in on-key-down
- sidebar-search*: cancel prior timer in on-key-press
- sidebar-content*: cancel prior timer in go-projects-with-key,
  go-fonts-with-key, go-drafts-with-key, go-libs-with-key

Each handler now stores the timer handle in a component-level ref and
disposes any pending handle before scheduling a new one.

AI-assisted-by: opencode-go/mimo-v2.5-pro

* ♻️ Refactor sidebar focus timer handling into helpers

Extract the repeated dispose-before-schedule focus idiom into
schedule-focus-by-id! (sidebar.cljs) and focus-and-untabbable!
(app.util.dom). Replaces the six duplicated blocks and adds
mf/use-effect unmount cleanup to dispose any pending timer in the
three sidebar components, closing the remaining leak noted in the
original fix.

AI-assisted-by: opencode/hy3-free

* ♻️ Extract use-focus-timer-ref hook for sidebar components

Replace the duplicated mf/use-ref + mf/use-effect cleanup pairs in
sidebar-project*, sidebar-search*, and sidebar-content* with a shared
use-focus-timer-ref hook (app.main.ui.hooks). The hook creates the ref
and disposes any pending timer on unmount via mf/with-effect, reading the
ref with mf/ref-val instead of deref. mf/use-effect is now a body-level
hook call rather than a let binding.

AI-assisted-by: opencode/hy3-free

* 📎 Add pr feedback fix
2026-07-28 11:06:00 +02:00
Andrey Antukh 7eb188b077 🐛 Fix text content repair and remove blast-radius containment (#10731)
Repair text shapes with empty/broken content at all three levels
(root, paragraph-set, paragraph) in migration 0025 to prevent
workspace update failures. Handle all corner cases: nil/empty/non-
vector children, non-map items, wrong types. Remove geometry-only
validation skip in changes.cljc so all shapes are validated.

AI-assisted-by: qwen3.7-plus
2026-07-28 11:00:53 +02:00
Andrey Antukh 535ccfb930 Merge remote-tracking branch 'origin/staging' into develop 2026-07-28 11:00:47 +02:00
Andrey Antukh 6c2b61e1ad 🐛 Fix several issues in RPC command handlers (#10670)
* 🐛 Fix several issues in RPC command handlers

- Reject circular library references in link-file-to-library
- Add explicit team permission check in search-files
- Constrain search-term max length to 250 chars
- Include :deleted-at in file ETag for COND caching
- Move storage I/O outside DB transaction in create-file-thumbnail

AI-assisted-by: deepseek-v4-pro

* 📎 Check perms before circular link checks

* 🐛 Handle circular library reference error

Catch :circular-library-reference error from backend when linking
files to libraries. Show user-friendly toast notification instead of
propagating unhandled error. Add English and Spanish translations.

AI-assisted-by: qwen3.7-plus
2026-07-28 10:59:16 +02:00
Andrey Antukh 458fa41036 Merge remote-tracking branch 'origin/main' into staging 2026-07-28 10:42:03 +02:00
Alejandro Alonso 6063a45c3c 🐛 Fix area selection aborted by select-shapes interrupt (#10870)
* 🐛 Fix area selection aborted by select-shapes interrupt

Only emit :interrupt from select-shapes when edition mode is active.
Unconditional :interrupt (from #10798) made drag-stopper cancel the
marquee mid-drag.

* 🔧 Fix text editor v2 fill e2e test on develop
2026-07-28 10:26:31 +02:00
Juanfran 68dccd6ac2 Degrade removed org owners to viewer instead of kicking them out
An organization owner keeps read-only access to the teams of their
organization even when they are not a member, so removing them from a
team was navigating them away from content they are still allowed to
see, and they could walk right back in through the URL.

Publish a :team-role-change to :viewer for them instead of a
:team-membership-change, which reuses the existing real-time role
transition on both the dashboard and the workspace. Any other member is
notified as before.

Signed-off-by: Juanfran <juanfran.ag@gmail.com>
2026-07-28 08:26:08 +02:00
María Valderrama 678e206fe6 🐛 Fix team last_activity_at calculation (#10854) 2026-07-27 15:31:56 +02:00
Andrey Antukh 63e0c536f0 Align event names column in format-last-events output
The third column (event name) in error report "last events" now starts at
a consistent position regardless of the delta value, by right-padding the
delta string to 10 characters. The first event always shows (+0ms).

Adds tests for empty, single, multi-event, and column alignment cases.

AI-assisted-by: deepseek-v4-flash
2026-07-27 13:24:11 +00:00
Alejandro Alonso ff16b8bbef 🔧 Fix text editor v2 fill e2e test on develop (#10848) 2026-07-27 13:25:35 +02:00
Andrey Antukh af120feb1f 🐛 Fix workspace crash and cleanup viewport_ref event/resize handling (#10721)
Replace `globals/document` and `globals/window` with
`js/document` and `js/window` in workspace.cljs, removing
the unused `app.util.globals` import. This avoids "can't
access dead object" errors in Firefox when navigating between
pages/files, matching the existing pattern used in
viewport/hooks.cljs.

Fix a leaked MOUSELEAVE listener in viewport_ref.cljs — the
ref callback added a new listener on every mount but never
unregistered the previous one. Now uses standard
.addEventListener/.removeEventListener with a React ref to
track the handler for proper cleanup.

Fix ResizeObserver cleanup in viewport_ref.cljs —
`init-observer` is now a private function that only creates
an observer when a node is provided, and cleanup is handled
via the ref callback on unmount.

AI-assisted-by: mimo-v2.5-pro
2026-07-27 12:11:12 +02:00
Andrey Antukh 5a0cee44b1 🐛 Harden frontend .getData call sites against undefined receivers (#10718)
* 🐛 Fix nil getData crash dropping ZIP without manifest.json

Add nil-guard in read-as-text to raise typed :invalid-entry error instead of calling (.getData nil writer) which produced a raw TypeError.

Made read-zip-manifest public (was defn-) with explicit detection of missing manifest.json, raising typed :invalid-penpot-file validation error. The existing catch path surfaces this hint as a friendly user error instead of the raw TypeError text.

Add regression tests for both paths. 374 users were affected, 704 occurrences across 2.17.0-RC2/RC3/RC4.

Fixes #10709.

AI-assisted-by: minimax-m3

* 🐛 Harden dnd/get-data against missing dataTransfer

When the sortable hook or any caller passes a synthetic event
without a dataTransfer property (e.g. a dragend fired after a drop
that has already cleared the transfer), the previous implementation
called .getData directly on the nil/undefined result and threw
"Cannot read properties of undefined (reading 'getData')".

Wrap the body in when-let so get-data returns nil cleanly when
dataTransfer is missing. All three current callers
(hooks.cljs:164, viewport/actions.cljs:531 and :562) already treat
the return value as optional via when-let / when, so no caller
breaks.

Add a regression test covering both the missing-dataTransfer case
and a real dataTransfer roundtrip.

AI-assisted-by: minimax-m3

* 🐛 Harden paste handler against missing clipboardData in forms

When a paste event arrives without a clipboardData property (e.g.
a programmatically dispatched ClipboardEvent in some browsers, or
edge cases like dragging a file with no text content), the previous
implementation called .getData directly on the nil/undefined
clipboardData and threw "Cannot read properties of undefined
(reading 'getData')".

Wrap the body in when-let so the paste logic is skipped entirely
when clipboardData is missing. The existing (string? paste-data)
guard in the inner when already tolerates nil; no other caller
behavior changes.

AI-assisted-by: minimax-m3

* 🐛 Harden paste handler against missing clipboardData in components/forms

Same defensive pattern as the main/ui/forms.cljs paste handler: wrap
the body in when-let so the .getData call is skipped when the
clipboardData property is missing on the paste event. Prevents the
raw "Cannot read properties of undefined (reading 'getData')"
TypeError for programmatic / edge-case paste events.

AI-assisted-by: minimax-m3

* 🐛 Harden v3 text editor paste and styles-fn against undefined receivers

Two related fixes for the "Cannot read properties of undefined
(reading 'getData')" family of bugs in the workspace text editor:

- v3_editor.cljs: wrap the paste body in when-let on clipboardData
  so .getData("text/plain") is never called on a nil receiver. The
  existing (when (and text (seq text))) guard already tolerates nil
  text; only the outer .getData call was unprotected.

- editor.cljs: add (and content ...) to the if branch in styles-fn
  so .getText and .getData are never called on a nil content. The
  else branch (legacy.txt/styles-to-attrs) is already the correct
  fallback for missing content.

Add a regression test that mirrors the fixed patterns and verifies
they no longer throw on synthetic events with no clipboardData or
nil content.

AI-assisted-by: minimax-m3

* 🐛 Harden get-editor-block-data and get-editor-block-type against nil block

getCurrentBlock from Draft.js can return undefined for an empty
selection (e.g. before any block is created). The previous
implementations called .getData / .getType directly on the result
and threw "Cannot read properties of undefined (reading
'getData')" / "...reading 'getType')".

Wrap both functions in (when (some? block) ...) so they return nil
cleanly. Callers in editor.cljs and text_editor.cljs already handle
nil results (render-block short-circuits via the case on type; the
text-data caller in text_editor.cljs lets nil flow up), so no
upstream change is required.

Add a regression test covering both functions with nil and
js/undefined input.

AI-assisted-by: minimax-m3

* 🐛 Harden draft-js block-data helpers against nil block

Three related fixes in the vendored draft-js package:

- mergeBlockData: early-return undefined when block is falsy.
  Without this, the first line (block.getData()) throws for callers
  that pass a nil block.

- splitBlockPreservingData: guard the blockMap.get(...) lookup. If
  the start key is stale (e.g. after a Modifier.splitBlock that
  doesn't actually produce the expected key), .get() returns
  undefined and the subsequent .getData() throws. Fall back to an
  empty Immutable Map for the block data.

- updateBlockData: short-circuit (return state unchanged) when
  mergeBlockData returns undefined. Without this, the chain
  newBlock.getData() would throw on the same nil-block case that
  mergeBlockData now guards.

These match the defensive nil-handling pattern used elsewhere in
the frontend (.getData callers) and protect against stale
selection keys in the Draft.js content state.

AI-assisted-by: minimax-m3
2026-07-27 10:56:27 +02:00
Luis de Dios 5ce206f904 🐛 Fix margin input order is inconsistent with padding input order (#10797) 2026-07-27 10:52:14 +02:00
Luis de Dios af51c897a8 🐛 Fix change defective onboarding image (#10783) 2026-07-27 10:51:26 +02:00
Andrey Antukh 44c15480f2 Merge remote-tracking branch 'refs/remotes/origin/develop' into develop 2026-07-27 10:48:54 +02:00
Andrey Antukh f60c82300a Merge remote-tracking branch 'origin/staging' into develop 2026-07-27 10:46:25 +02:00
Andrey Antukh aad9bc8f65 📎 Fix playwright version 2026-07-27 10:45:59 +02:00
Elena Torró e17f0ea7ee 🐛 Fix selrect misplacement on grid and flex layouts (#10832)
* 🐛 Fix selrect misplacement on grid and flex layouts

* ♻️ Refactor clear-transform-preview to use it from both ui and data layers
2026-07-27 10:25:05 +02:00
María Valderrama bb8f965cf0 🐛 Hide Organizations header/divider when org list is empty (#10833) 2026-07-27 09:54:04 +02:00
Dr. Dominik Jain a749763fb1 ⬆️ Upgrade plugin-types and plugin-styles to v1.5.0 in MCP plugin (#10830)
The upgrade allows ts-ignore annotations and manually declared type information
to be removed.
2026-07-27 09:10:04 +02:00
Andrey Antukh 2f8d71e143 Merge remote-tracking branch 'origin/staging' into develop 2026-07-27 09:02:56 +02:00
Andrey Antukh 0b072b22b0 📚 Update changelog 2026-07-27 09:01:43 +02:00
Andrey Antukh 170d129a5a Merge remote-tracking branch 'origin/staging' 2026-07-27 08:52:50 +02:00
Andrey Antukh eda5aa76f0 Merge remote-tracking branch 'origin/main' into staging 2026-07-27 08:52:31 +02:00
Andrey Antukh b54c1f316a Add minor improvements for error report script 2026-07-25 09:56:38 +02:00
María Valderrama 66bb6a11df 🐛 Make organization logos publicly accessible for invitation emails (#10831) 2026-07-24 14:08:41 +02:00
María Valderrama 62880cb58c 🐛 Fix downgrade to Professional bypassing warning modal (#10814)
* 🐛 Fix downgrade to Professional bypassing warning modal

* 📎 Code review
2026-07-24 14:08:11 +02:00
Elena Torró 8142f4949e 🔧 Revert stroke to path flag deletion and enable it by default (#10827) 2026-07-24 13:40:47 +02:00
c10c7b08aa 🐛 Fix radial gradient handles on rotated ellipses (#10666)
The handle transform composed rotation incorrectly, so handles blew out in size when an ellipse was rotated.

Fixes #10069

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-24 13:29:37 +02:00
Belén Albeza 56c3e9777e 🐛 Fix blinking when zooming in / out in the text editor (#10816) 2026-07-24 13:10:31 +02:00
8b485b4a8b 🐛 Preserve token references when copying and pasting properties (#10665)
Copy/paste of properties resolved tokens to their values, dropping the reference. Carry the token with the value it resolves, at sub-attribute granularity for map-valued attrs.

Fixes #9582

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-24 13:08:01 +02:00
Eva Marco d94b139071 🐛 Fix text edition state when relesecting (#10798)
* 🐛 Fix text edition state when relesecting

* 🐛 Fix CI
2026-07-24 12:15:16 +02:00
Marina López 3610b81e4b 🌐 Localize Nitrate subscription flows 2026-07-24 11:02:58 +02:00
Eva Marcoandalonso.torres 5de06e8f77 🎉 Add customizable shortcuts (#10237)
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-07-24 10:36:32 +02:00
Dr. Dominik Jainandalonso.torres caacd482af Add composable test framework plugin (applied to component tests, runs in CI) (#10679)
*  Add plugin with composable test framework and component tests

The plugin provides a framework for writing composable tests against
the Plugin API, and applies it to systematic end-to-end testing of
component semantics.

The framework's core ideas: a test is written once as a composition of
operations over a starting configuration; choice points among the
operations (optional steps, alternatives) expand the composition into
a full sweep of test variants, so a single case definition yields
broad combinatorial coverage; and the operations drive the real Plugin
API with real change propagation, testing the full production
implementation.

The initial application is a suite of component test cases covering
synchronization, overrides, swap slots and variants — the
TypeScript/e2e continuation of the ClojureScript composable test suite
(frontend_tests.composable_tests). Several cases originate from
reproducing real defects (e.g. #10109 and the swap-slot corruptions).

Tests run from an interactive panel in Penpot: cases are listed with
plain-language descriptions, tests can be run selectively, results
stream in live, and every checkbox carries a stable DOM id — so the
panel can equally be driven programmatically (the basis for running
the suite in CI), as documented in the plugin's README.

Lives at plugins/apps/composable-test-suite as a regular member of the
plugins workspace (init script, start:plugin:composable-test-suite,
shared dev port 4202, covered by build:plugins via the new
./apps/*-test-suite filter).

Related to #10584.

AI-assisted-by: claude-fable-5

*  Run the composable test suite headlessly in CI

Adds a headless run mode for the composable test suite, following the
plugin-api-test-suite's CI architecture, and a workflow that runs it as
a per-PR gate.

An in-sandbox entry (src/ci/headless.ts) runs the suite without the
panel UI — the framework's runner was UI-free by construction, so no
refactoring was needed — and streams each result through console
markers, addressed by the same composite identifiers the panel uses
(e.g. MainEditSyncs-2), with durations and, on failure, the error and
the applied-steps transcript. It is built as a single self-executing
bundle and evaluated directly inside a real Penpot plugin sandbox by
the driver (ci/run-ci.ts), so no plugin dev server or port is involved.

The driver needs no backend and no login: it serves the prebuilt
frontend bundle via the frontend e2e static server and intercepts every
backend RPC with Playwright fixtures. The mocked backend is not a
limitation for this suite — everything it asserts is frontend store
logic executed in memory — which the full run confirms: all 48 tests
behave identically to the interactive panel, including variants and
swap slots, with the single (currently expected) failure of
MainEditSyncs-2 reproducing bug #10109 under the mock.

TEST_FILTER selects tests by identifier substring; CI_TIMEOUT_MS bounds
the run. The mock harness mirrors the frontend e2e harness (see the
provenance note in the driver).

Related to #10584.

AI-assisted-by: claude-fable-5

* 📚 Restructure the composable-tests memory around both suites

Present the composable component tests top-down: the shared framework
principles upfront, then the two implementations — the ClojureScript
suite in the frontend test tree and the TypeScript suite in the plugin,
which tests fully end-to-end with a slightly more elaborate set of
abstractions — and the plugin's headless CI run, pointing to the
plugin's README for operational details. Also records this session's
additions (geometry operations, case N, the CI harness).

AI-assisted-by: claude-fable-5

* 📎 Refine the PR-description conventions in the creating-prs memory

Encourage digestible descriptions: bullet items over prose (grouped by
area with bold lead-ins for larger PRs) and no manual line wraps, since
the rendered markdown adapts to the viewport. Also drop the outdated
'MCP' from the standard Note line.

AI-assisted-by: claude-fable-5

* 🔧 Set Prettier endOfLine to auto in plugins workspace

Prettier defaults to endOfLine "lf", which is incompatible with
checkouts on Windows that use core.autocrlf=true

* 🐛 Fix problems with suite

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-07-24 09:59:18 +02:00
Belén Albeza 4f46700c94 🐛 Fix text editor v3 blinks (#10654) 2026-07-24 09:14:59 +02:00
Andrey Antukh c467d98c9e Merge remote-tracking branch 'origin/staging' into develop 2026-07-23 17:06:56 +02:00
Andrey Antukh d5d6c61ba4 💄 Remove unused app.common.pprint import in errors.cljs
AI-assisted-by: deepseek-v4-flash
2026-07-23 15:05:18 +00:00
ParinithandAndrey Antukh 6e85ce5d7a 🐛 Center canvas and select layer when navigating search results (#10453)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 15:47:57 +02:00
Andrey Antukh f7c312021b Improve error-reports CLI with streaming, time-range, and stats
Server changes:
- Switch list ordering from DESC to ASC (oldest first)
- Flip cursor direction to > for forward pagination
- Add 'until' param for server-side upper-bound filtering

CLI changes:
- Add --from/--to flags mapping to server's since/until
- Streaming output for --all and --format ndjson
- Add --format ndjson option (one JSON object per line)
- Add --normalize-hints flag to strip dynamic values
- Add --output flag to write list results to file
- Add 'stats' subcommand with aggregations (signature, host,
  tenant, version, source, kind, hour) reading from API, file, stdin
- stats input supports JSON, JSON array, and NDJSON formats

Test changes:
- Fix pagination assertions for ASC ordering

AI-assisted-by: mimo-v2.5-pro
2026-07-23 13:35:09 +00:00
Lucas OzdemirandAndrey Antukh 3fa7943344 🐛 Allow SVG files for image fills (#10771)
Use Penpot's shared image type list for image fill uploads and add an integration test covering SVG file selection.

AI-assisted-by: gpt-5.6-sol

Signed-off-by: Lucas Ozdemir <lulu.58@outlook.fr>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 13:32:06 +02:00
19bdd7081b 🐛 Do not apply the new-password policy to the old-password field (#10661)
The change-password form validated the existing password against the 8-character policy, locking out accounts whose current password predates it.

Closes #10626

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 13:31:38 +02:00
Andrew CunliffeandAndrey Antukh ed85d1d1af 🐛 Fix color token sets rendering in ascending-precedence order (#10658)
The Color tokens picker showed token sets in their raw definition
order (ascending precedence), so the lowest-precedence set appeared
first and the highest-precedence (last-defined, winning) set
appeared last. This is the opposite of what's useful: users care
most about which set is currently winning, so that one should be at
the top.

get-sets returns sets in definition order and the picker's
grouped-tokens-by-set pipeline (add-tokens-to-sets ->
filter-active-sets -> filter-non-empty-sets -> group-sets ->
combine-groups-with-resolved) preserves that order at every step, so
the picker just rendered get-sets' raw order. Reverse the set seq
once, before it enters the pipeline, so the highest-precedence set
renders first.

group-sets groups sets by parent path via group-by, which risked
restoring definition order within a subgroup independent of the
reversed input order. Added tests covering a flat set list, a
reversed subgroup (to confirm group-by does not silently re-sort
subgroup members), and a mixed flat/subgrouped list.

Closes #10552

Signed-off-by: Andrew Cunliffe <cunliffeandrewc@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 13:30:32 +02:00
d42f78b80e 🐛 Fix clipboard crash when copying as SVG (#10663)
* 🐛 Fix clipboard crash when copying as SVG

clipboard.write with an image/svg+xml payload throws an unhandled DOMException on browsers that do not support the type. Fall back to writeText for that specific failure.

Fixes #10596

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>

* 📎 Update Kaleidos Copyright

Signed-off-by: Akshit Nassa <nassaakshit@gmail.com>

---------

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Signed-off-by: Akshit Nassa <nassaakshit@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 13:29:59 +02:00
Andrey Antukh 45405a018b 📎 Add minor changes on error reports 2026-07-23 13:06:46 +02:00
Alejandro Alonso 8bf411c347 🐛 Fix viewer wasm position data init (#10805) 2026-07-23 12:58:20 +02:00
Andrey Antukh 9b88d35664 Merge remote-tracking branch 'origin/main' into staging 2026-07-23 11:02:56 +02:00
Andrey Antukh e4d88b3ab4 🐛 Show proper version on error report api 2026-07-23 11:02:27 +02:00
Andrey Antukh 66b978c01e Add wall-clock timestamps to last-events buffer
Each event entry in `last-events` is now wrapped as
`{:name <event-type> :t (app.common.time/now)}` so every event carries a
wall-clock timestamp. A new helper `format-last-events` renders the
buffer as a multi-line string with ISO time and delta-since-previous-
event in ms, replacing the previous pprint dump in error reports.

This lets support/devs tell whether the events leading up to a crash
were spaced out (user action) or jammed together (runaway loop).

AI-assisted-by: minimax-m3
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-07-23 10:50:06 +02:00
Andrey Antukh 52e5e0bec6 🐛 Add more fields on table format on errors report cli client 2026-07-23 10:46:17 +02:00
Andrey Antukh 7430ffe718 ⬆️ Update opencode on devenv 2026-07-23 10:46:05 +02:00
Elena Torró 09730d10d0 Make stroke to path available without config flag (#10792) 2026-07-23 10:27:14 +02:00
Andrey Antukh b3091399cd Add timestamp to the frontend error report (#10772) 2026-07-23 10:26:41 +02:00
Andrey Antukh b6629c0034 🐛 Mark non-Penpot zip files as unknown in import worker (#10782)
Non-export zip files were tagged as :legacy-zip with the body attached,
causing downstream parsing to crash on unrecognized zip content. Now
they are marked :unknown, matching how other unrecognized formats are
handled, so the import fails gracefully.

AI-assisted-by: deepseek-v4-flash
2026-07-23 10:26:22 +02:00
Luis de Dios c57f90d4a2 🐛 Fix main toolbar overlaps grid edition bar (#10789) 2026-07-23 10:25:47 +02:00
Andrey Antukh 707cfac375 🐛 Throttle nudge stream to cap re-renders under fast key-repeat (#10736)
Holding an arrow key on a selection with a fast OS key-repeat rate
crashed the workspace with React error #185 (Maximum update depth
exceeded): each OS key-repeat event was converted 1:1 into a
`set-modifiers`/`set-wasm-modifiers` store write inside
`nudge-selected-shapes` with no throttle, starving the renderer.

The sibling mouse-driven resize/rotate/move paths got an `rx/sample`
throttle in PR #10560; the keyboard-nudge path was the only transform
stream left un-throttled. This change applies the same `rx/sample`
throttle to the nudge stream, mirroring the drag-path structure, and
adds a regression test guarding the final committed position
invariant under a burst of 20 `move-selected` events for both the WASM
and legacy (non-WASM) branches.

Closes #10726

AI-assisted-by: glm-5.2
2026-07-23 09:57:28 +02:00
Juanfran 4900d4b24a Gate nitrate bulk-create-profiles behind a flag (#10785)
The bulk profile creation endpoint creates already active profiles that
skip email verification and onboarding, so it should not be reachable on
production deployments. Add the `nitrate-bulk-create-profiles` flag,
disabled by default, and reject the call when it is not enabled.

Signed-off-by: Juanfran <juanfran.ag@gmail.com>
2026-07-23 09:33:32 +02:00
Andrey Antukh 702a435569 📎 Add token id to the dom for more easy identify the token id 2026-07-23 09:29:45 +02:00
Andrey Antukh bac739717c 📎 Do not print exeption when cant setup reloading
happens only when production jar is executed
2026-07-23 09:29:09 +02:00
Andrey Antukh 4c222c469a 🐛 Move to runtime the repl reploading config 2026-07-23 09:04:57 +02:00
Andrey Antukh b1ccb252fd 📎 Update review command and code-quality skill 2026-07-23 08:31:36 +02:00
Elena Torró 4383cf183a Add background blur to strokes (#10716) 2026-07-22 17:07:16 +02:00
Andrey Antukh eb1e0ad186 🐛 Guard team-container* when team-id is not a uuid (#10645)
The dashboard route can be reached without a `:team-id` query parameter
(e.g. `/#/dashboard/recent`). When that happened, `team-container*` was
emitting `dtm/initialize-team` with a `nil` team-id, which set
`:current-team-id` to `nil` in the application state. The dashboard
and workspace initialize events then built `df/fetch-fonts` with a
`nil` team-id, producing a `:get-font-variants` RPC with empty params
`{}` that the backend rejected with HTTP 400.

Guard `team-container*` so it does not emit `initialize-team` /
`finalize-team` and does not render the children when `team-id` is
not a uuid. The `with-effect` body and the render are guarded
independently; the cleanup closure captures the same `team-id` as the
setup, so the finalize still fires correctly when transitioning between
valid teams.

AI-assisted-by: minimax-m3
2026-07-22 16:57:46 +02:00
Belén Albeza 7b9896ab32 🐛 Fix tabindex error (#10780) 2026-07-22 16:31:20 +02:00
Andrey Antukh 2523a72c32 :boolk: Update changelog 2026-07-22 16:26:14 +02:00
Andrey Antukh e076443eca Merge remote-tracking branch 'origin/main' into staging 2026-07-22 16:23:48 +02:00
Elena Torró ef3511e519 Add basic individual strokes implementation (#10648) 2026-07-22 16:13:38 +02:00
Alejandro Alonso f21bd45893 🐛 Fix paths and layout performance and rendering on boolean exclusions (#10778)
* 🐛 Skip identity transforms in layout reflow propagation

Layout reflow emitted identity transforms for unchanged children, which
fanned out through the whole subtree on every drag frame and froze large
files in the WASM renderer.

* 🐛 Fix exclude boolean rendering in render WASM
2026-07-22 15:47:43 +02:00
Andrey Antukh 40ab48ea01 🐛 Fix inconsistencies on serenea memories 2026-07-22 15:23:25 +02:00
Marina López 396d799c71 🐛 Redesign my penpot review 2026-07-22 14:45:14 +02:00
María Valderrama f20e4280fc 🐛 Fix theme-aware illustration in nitrate modal (#10773) 2026-07-22 14:44:26 +02:00
Andrey Antukh 2344ba22a6 🎉 Add error reports API and CLI tool
Implement RPC methods for querying server error reports with pagination
and filtering. Add CLI tool (tools/error-reports.mjs) for convenient
access with table and JSON output formats. Extract profile-id from audit
events and logging context for better error categorization. Build
improved HREF using request path when available.

AI-assisted-by: qwen3.7-plus
2026-07-22 14:18:51 +02:00
Andrey Antukh 018d840bab ♻️ Refactor internal organization of system initialization
Add the ability to suspend and add nrepl to the whole system

AI-assisted-by: qwen3.7-plus
2026-07-22 14:18:51 +02:00
Andrey Antukh e1f976aa2f Merge remote-tracking branch 'origin/staging' 2026-07-22 14:01:42 +02:00
Andrey Antukh 80c84a3331 🐛 Strip Authorization header when proxying asset redirects to S3 (#10777)
When nginx follows a backend 307 redirect to a presigned S3 URL, it was
forwarding the client's Authorization header to S3. Production S3 rejects
this because it sees two auth mechanisms (presigned URL signature +
Authorization header). MinIO in devenv is more lenient and ignores the
extra header.

Fix: add proxy_set_header Authorization "" in the @handle_redirect block.

Fixes #10776

AI-assisted-by: mimo-v2.5-pro
2026-07-22 14:00:35 +02:00
Andrey Antukh 9cb039070f ♻️ Refactor nitrate audit method and tests
Keep the audit refactor lint-clean after rebasing onto develop.

AI-assisted-by: gpt-5
2026-07-22 13:11:57 +02:00
Marina López 9f029de578 Handle penpot events from nitrate 2026-07-22 13:11:57 +02:00
Pablo Alba 3a0aca52c3 Add new debugger tool components-debugger (#10757) 2026-07-22 10:49:18 +02:00
Andrey Antukh f9439d2942 Merge remote-tracking branch 'origin/staging' into develop 2026-07-22 10:48:40 +02:00
Andrey Antukh e4cddd8536 📚 Update serena memories 2026-07-22 10:45:18 +02:00
Andrey Antukh 72f3165341 📎 Update changelog 2026-07-22 10:45:00 +02:00
Andrey Antukh f98b1ddb8e Merge remote-tracking branch 'origin/main' into staging 2026-07-22 10:36:47 +02:00
Elena Torró 08e42687d1 ♻️ Extract GPU-free RenderResources; add headless wasm exports (#10653) 2026-07-22 09:56:21 +02:00
Dr. Dominik Jain d4e87ec59d ⬆️ Update Serena to 1.6.1 in agentic devenv (#10770)
Update project files accordingly
2026-07-22 09:35:50 +02:00
AKandAkshit Nassa b8e3089ae7 Make plugin API validation errors precise and crash-safe (#10667)
Schema validation reported a generic "Invalid data" message. Report the expected schema and the received value, with a bounded cycle-safe renderer so the error path cannot itself crash.

Fixes #10072

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
2026-07-22 09:29:19 +02:00
Andrey Antukh 1c917951b6 Merge remote-tracking branch 'origin/staging' into develop 2026-07-22 09:18:25 +02:00
Andrey Antukh f3bf24b4f6 ♻️ Consolidate dev tooling into scripts/ and reorganize docs
Move all development tools from tools/ to scripts/ for consistency.
Rename lint/fmt/check-fmt to lint-clj/fmt-clj/check-fmt-clj to clarify
they target Clojure specifically. Remove unused scripts (attach-opencode,
start-opencode, start-opencode-server) and the backport-commit skill.

Update all internal references across .serena/, AGENTS.md, and
CONTRIBUTING.md to point to the new script locations. Simplify
CONTRIBUTING.md by delegating module-specific fmt/lint instructions
to the respective serena memories.

AI-assisted-by: deepseek-v4-flash
2026-07-22 09:18:06 +02:00
Andrey Antukh 73bfc0dc15 🐛 Guard workspace-page* when file-id is not a uuid (#10655)
The workspace route can be reached without a `:file-id` query parameter
(e.g. `/#/workspace?team-id=...`). When that happened, `workspace*` was
emitting `dw/initialize-workspace` with a nil file-id, which stored nil
in `:current-file-id`. The `fetch-profiles` event then read nil from
state and called `:get-profiles-for-file-comments` with `{:file-id
nil}`, producing a 400 response.

Move the `use-equal-memo` calls for `file-id` and `page-id` from
`workspace*` up to `workspace-page*`, and guard the render with
`(when (uuid? file-id) ...)` so `workspace*` only mounts when `file-id`
is a valid uuid. Since `workspace*` never mounts with a nil `file-id`,
`initialize-workspace` is never emitted with nil, and the 400 is
prevented at the source.

AI-assisted-by: minimax-m3
2026-07-22 09:00:43 +02:00
Andrey Antukh bdce5817ea 📚 Update changelog 2026-07-22 08:43:47 +02:00
Andrey Antukh 4ecbac896f Merge remote-tracking branch 'origin/staging' 2026-07-22 08:43:03 +02:00
Juan de la CruzandLuis de Dios 5dff551f31 🎉 Add page multi-selection in the workspace sitemap (#10581)
* 🎉 Add page multi-selection in the workspace sitemap

* ♻️ Simplify page selection state updates with single assoc

* 🐛 Fix SCSS issue

* ♻️ Update some components to new syntax

* ♻️ Adapt SCSS to the new guidelines

* ♻️ SCSS cleanup

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-07-21 14:26:03 +02:00
Juan de la CruzandLuis de Dios fa996ab240 Add list view toggle for dashboard files (#10692)
*  Add list view toggle for dashboard files

*  Add drop files visual feedback

* ♻️ Use radio buttons component from DS

* ♻️ Use hook to keep layout status

* ♻️ Refactor code and SCSS

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-07-21 13:16:10 +02:00
Pablo Alba 0ff73ae711 Add prune-unrelated-items debug utility (#10687) 2026-07-21 12:31:23 +02:00
Alonso Torres 6c5d01283a 🐛 Add more test for reported bugs (#10765) 2026-07-21 12:18:40 +02:00
Andrey Antukh 64026fc1f6 📎 Update changelog 2026-07-21 11:11:21 +02:00
María Valderrama 2de08f00e8 🐛 Fix teams' inactive status for nitrate (#10763) 2026-07-21 10:23:03 +02:00
Dr. Dominik Jainandalonso.torres 4ea56e0b89 🐛 Fix propagation of geometric changes to rotated component copies (#10574)
* 🐛 Fix geometry sync between mains and rotated component copies

Rotating a copy instance as a whole marked every shape inside it as
touched for geometry, so later geometric changes in the main (e.g. a
resize) were no longer propagated to that copy, while non-geometric
ones (e.g. fills) still were. And on paths where geometry did get
written to a rotated copy (e.g. resetting overrides), the sync engine
compensated only the roots' position delta, so the written values wiped
the copy's rotation back to 0.

Model the instance root's transformation as inherited, overridable
content, asymmetric to position (which remains free per-instance
placement):

- An untouched copy follows the main's transformation verbatim,
  including rotation and flips (preserving the BUG #13267 semantics
  that rotating a main propagates to its copies).

- Transforming a copy as a whole overrides only its ROOT: check-delta
  compares the root's rotation/flips absolutely, but the descendants
  relative to their root, so they merely follow and stay untouched.

- When a copy root's geometry is overridden, update-attrs expresses the
  main's geometry in the copy's own frame: reposition-shape applies the
  roots' relative transformation (rotation/flips) around the dest root
  center in addition to the position delta. Geometric changes from the
  main then keep propagating to the rotated copy, landing correctly in
  its rotated frame instead of destroying its placement.

Covered by the new composable test case
case-n-geometry-sync-with-rotated-instances: an 8-variant sweep over
optional copy rotation, optional main rotation, and one of a fills or
height edit on the main child, asserting the whole model through the
real workspace events (the new rotate operation dispatches
dwt/increase-rotation, whose apply-modifiers step runs the check-delta
classification under test; change-height dispatches
dwt/update-dimensions and implements IPropertyCheck so one-of sweeps
can mix property and geometry edits). Verified by temporarily reverting
the fix: the case then fails with 6 assertion failures and passes again
with the fix restored.

Fixes #10109

AI-assisted-by: claude-fable-5

* 🐛 Fix synchronization problems

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-07-21 10:05:31 +02:00
Marina López 27392abd49 🐛 Increase team name abbreviation limit in invitation emails 2026-07-21 09:54:06 +02:00
Alejandro Alonso 6c5618025d Merge pull request #10712 from penpot/elenatorro-10706-apply-background-blur-to-text-shapes
 Add background blur to text shapes
2026-07-21 09:53:01 +02:00
Andrey Antukh 4dc59ede50 Merge remote-tracking branch 'origin/main' into staging 2026-07-20 21:49:46 +02:00
Andrey Antukh 378d97df93 📎 Update refine-prompt skill 2026-07-20 21:49:19 +02:00
Alonso Torres 79a471a6f1 🐛 Fix default exporter config (#10761) 2026-07-20 21:28:24 +02:00
f2a9dd1a08 🐛 Reject invalid formulas on numeric inputs (#10659)
Relative operators were accepted in operand position, so "10+*3" silently evaluated to 310 instead of being rejected. Make negation a first-class operand so legitimate negative operands ("10 + -3") keep working.

Fixes #9581

Signed-off-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Akshit Nassa <akshitnassa412@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-20 16:15:29 +02:00
María Valderrama f06339fb87 Update enterprise modals illustration (#10759) 2026-07-20 16:03:40 +02:00
Andrey Antukh c1d09c80a6 Merge remote-tracking branch 'origin/staging' 2026-07-20 14:49:21 +02:00
Andrey Antukh e69fdef1dc Merge remote-tracking branch 'origin/main' into staging 2026-07-20 14:48:39 +02:00
Andrey Antukh 4c7863d6c8 📚 Add .penpot format docs and inspector tool (#10674)
* 📚 Add comprehensive documentation for .penpot file format (v3)

Create user-facing documentation for the .penpot binfile format to help
developers and power users understand and inspect the ZIP+JSON structure.

- Add technical specification with complete schema reference for all JSON
  files (manifest, file metadata, pages, shapes, library assets, storage
  objects, plugin data)
- Add user-friendly overview explaining the format structure, inspection
  methods, and version history
- Update export-import-files.njk to clarify current format is not deprecated
  and note that v2 was never released
- Add cross-links between documentation pages
- Include source code references to authoritative malli schemas

AI-assisted-by: qwen3.7-plus

* 📎 Add playwright dependency to the root package.json

* 📚 Add browser based .penpot file inspector to docs

Add a client-side, no-build inspector for .penpot (v3) files. The tool
runs entirely in the browser: JSZip is loaded lazily from a CDN, the
file is never uploaded. It provides a collapsible file tree, syntax-
highlighted JSON viewer with search, image previews, a summary panel
with file/shape/storage counts, shape summary cards with color swatches,
and clickable UUID cross-references with backlinks.

The inspector is added at
docs/technical-guide/developer/data-model/penpot-file-inspector.njk
and linked from the format spec and the user-facing format page.

AI-assisted-by: minimax-m3

* 📚 Use full page width and 2-column layout for inspector

The inspector previously sat inside the docs site's 42rem content
column, which forced a stacked layout and wasted the wide viewport.

- Hide the docs page-navigation sidebar on this page via :has()
- Use a 2-column CSS grid: file tree (280px, sticky) on the left,
  content (flex: 1) on the right
- Restore the directory tree as a permanent left-rail navigation
  (it was removed when the picker was first introduced, then added
  back as a collapsible panel; a sticky rail is a better fit now
  that horizontal space is plentiful)
- Remove the tree toggle button — the tree is always visible
- Collapses to 1 column on viewports below 900px

AI-assisted-by: minimax-m3

* 💄 Indent nested JSON values in inspector

The JSON tree view in the inspector was rendering all properties at
the same indent level, making it hard to see which values were
nested inside objects or arrays. Root-level properties and deeply
nested ones looked identical.

Add padding-left and a subtle vertical guide line to .jchildren so
each nesting level is visually distinct. Bump the toggle column to
1.2em and add a touch of vertical padding to .jrow for breathing
room.

AI-assisted-by: minimax-m3
2026-07-20 14:45:59 +02:00
Andrey Antukh 766368567e 🐛 Ensure text position-data always includes :fills key (#10650)
Position-data entries for text shapes could omit the :fills key when the
element had no explicit fills, causing backend malli validation failures.

- Use get-default-text-attrs as base in WASM calculate-position-data
- Default fills when CSS --fills property is absent in DOM calc path

AI-assisted-by: deepseek-v4-pro
2026-07-20 14:45:36 +02:00
Andrey Antukh c6de3ce47b 📎 Do not drop uuid-ossp extension on migrations
Requires specific permissions
2026-07-20 14:05:04 +02:00
Luis de Dios a0b5c23f99 🐛 Fix close comments panel button does not work (#10651) 2026-07-20 13:10:27 +02:00
Dr. Dominik JainandMichael Panchenko 51c0129485 Simplify MCP client setup, adding client-setup command (#10604)
Add a `client-setup` command to the published `@penpot/mcp` package.
When invoked as `penpot-mcp client-setup`, the bin delegates to the `add-mcp`
CLI against the local MCP server URL (from `PENPOT_MCP_SERVER_PORT`, default
4401), run via npx.

Update docs on MCP client configuration.
Recommend calling `add-mcp` directly for now, since the `client-setup` command
only becomes available once a new `@penpot/mcp` release is published to npm.

AI-assisted-by: Claude

Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de>
2026-07-20 13:08:51 +02:00
Belén Albeza 4c21923c4a 🐛 Fix accent menu (MacOS) inserting an extra character (#10637) 2026-07-20 12:16:52 +02:00
Andrey Antukh 5f35fdf217 ♻️ Replace uuid-ossp defaults with gen_random_uuid() and add missing :id on insert (#10591)
* 📎 Add postgresql client tool wrapper for devenv

* ♻️ Replace uuid-ossp defaults with gen_random_uuid() and add missing :id on insert

- Switch all DEFAULT uuid_generate_v4() to gen_random_uuid()
  (built-in PG 13+, no extension required)
- Add explicit :id (uuid/next) to 4 db/insert! calls that were
  relying on the DB default (team-profile-rel, project-profile-rel,
  team-project-profile-rel)
- Drop uuid-ossp extension (no longer needed)
- Add missing uuid require to projects.clj and srepl/binfile.clj

AI-assisted-by: deepseek-v4-flash

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-07-20 12:14:44 +02:00
Andrey Antukh 26c4ec18fe 🐛 Return 400 instead of 500 when ImageMagick fails on invalid images (#10643)
When ImageMagick fails to process an uploaded image (e.g., corrupted PNG
with invalid IHDR data), the backend was raising :type :internal with
:code :imagemagick-error, which mapped to HTTP 500. The frontend treated
this as a server error and displayed the full error page.

Changed exec-magick! to raise :type :validation with :code :invalid-image
instead. This flows through the existing :invalid-image handler in
errors.clj which returns HTTP 400. The frontend's handle-media-error and
process-error now catch this code and show a notification banner.

AI-assisted-by: qwen3.7-plus
2026-07-20 11:56:07 +02:00
Sangeeth Thilakarathnaandsanmaxdev 5dadda0f2b 📚 Improve plugin deployment documentation (#10544)
Co-authored-by: sanmaxdev <sanmaxdev@users.noreply.github.com>
2026-07-20 11:55:36 +02:00
Andrey Antukh 5acc3aac99 Merge remote-tracking branch 'origin/staging' into develop 2026-07-20 11:48:26 +02:00
Andrey Antukh fc6b3ee7f0 🐛 Demote OIDC userinfo 401 errors to warning and add comprehensive test coverage (#10636)
* 🐛 Demote unable-to-retrieve-user-info OIDC error to warning level

401 responses from the OIDC userinfo endpoint (e.g. expired/revoked GitHub
token) are normal auth failures, not server errors. Logging at :error level
triggers the database and Mattermost error reporters unnecessarily.

AI-assisted-by: deepseek-v4-flash

*  Add pure function tests for OIDC auth module

Add tests for: int-in-range?, valid-info?, qualify-prop-key, qualify-props,
provider-has-email-verified?, profile-has-provider-props?, redirect-response,
redirect-with-error, redirect-to-verify-token, and build-redirect-uri.

AI-assisted-by: deepseek-v4-flash

*  Add HTTP-mock tests for fetch-user-info and fetch-access-token

Replace with-redefs with binding (cf/config is ^:dynamic).
Add tests for: fetch-user-info (success, 401, 500, request structure),
fetch-access-token (success, 400 error).

AI-assisted-by: deepseek-v4-flash

*  Add get-info integration tests with partial mocking

Test all branches: token/userinfo/auto info sources, incomplete info,
role checks (satisfied and insufficient), state props merge,
sso-session-id from claims, and sso-provider-id for uuid providers.

AI-assisted-by: deepseek-v4-flash

*  Add callback-handler integration tests with real tokens and session

Tests all main branches: error param, no profile (registration disabled),
profile blocked, provider mismatch, inactive profile, success flow,
and graceful handling of unable-to-retrieve-user-info exception.
Uses real tokens/generate, tokens/verify, and session/inmemory-manager.

AI-assisted-by: deepseek-v4-flash
2026-07-20 11:46:20 +02:00
Elena Torró 8d98877ba7 🐛 Fix using SVG image file as shape fill (#10707) 2026-07-20 10:41:07 +02:00
Shlok GoyalandAndrey Antukh 826072c65d Remove misleading MCP config from success modal (#10415)
Signed-off-by: Shlok1729 <shlokgoyal1279@gmail.com>
Signed-off-by: Shlok Goyal <shlokgoyal1279@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-20 10:07:26 +02:00
Juan de la Cruzandalonso.torres cddbd8e897 Add font family preview in typography selector (#10411)
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-07-20 10:06:02 +02:00
Yaron Shahrabani 29ed79fcdc 🌐 Add translations for: Hebrew
Currently translated at 85.9% (2036 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-07-20 10:01:22 +02:00
Andrey Antukh a96001894c 📚 Add explicit AI-assisted-by format rules to commit memory
AI-assisted-by: mimo-v2.5
2026-07-20 07:43:27 +00:00
Andrey Antukh fd5d72cedd 📎 Update common test script to match package.json changes
Replace removed test:js and test:jvm npm scripts with pnpm run test
and direct clojure -M:dev:test invocation.

AI-assisted-by: mimo-v2.5
2026-07-20 07:42:27 +00:00
Andrey Antukh b2140d5ff1 📎 Update opencode on devenv 2026-07-20 09:38:18 +02:00
jonnysemon 9aac607552 🌐 Add translations for: Arabic
Currently translated at 47.4% (1124 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2026-07-18 05:01:31 +02:00
Mahmoud A. Rabo 4ea83d31fc 🌐 Add translations for: Arabic
Currently translated at 47.4% (1124 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2026-07-18 05:01:24 +02:00
Marina López 33d478b532 ♻️ Rename org to organization 2026-07-17 11:36:08 +02:00
Dr. Dominik Jain d9511db585 🔧 Auto-confirm pnpm modules purge in MCP bootstrap #10680 (#10681)
pnpm occasionally detects an incompatible node_modules directory (e.g.
after a store location or pnpm major version change) and interactively
asks whether to remove and recreate it, blocking the MCP bootstrap in
the devenv tmux pane. Set confirmModulesPurge: false in
mcp/pnpm-workspace.yaml so the purge is auto-confirmed; this file is
included in the npm pack tarball (unlike .npmrc) and applies to all
install invocations from a single place.

AI-assisted-by: claude-fable-5
2026-07-16 17:00:20 +02:00
Andrey Antukh a4347451d0 Merge remote-tracking branch 'origin/staging' into develop 2026-07-16 16:57:32 +02:00
Andrey Antukh 779983d38e 📎 Standardize test scripts and add execution discipline docs
- Remove conditional build from test scripts (frontend, common)
- Remove test:jvm from common package.json (JVM tests via clojure directly)
- Remove test from backend package.json (JVM tests via clojure directly)
- Unify common/scripts/test-quiet.js with frontend's BUILD_STEPS pattern
- Add execution discipline section to mem:testing (no piping, tee to file)
- Add READ mem:testing FIRST directives to module testing docs

AI-assisted-by: deepseek-v4-flash
2026-07-16 14:57:05 +00:00
Kobi Hikri 7e36192034 🔧 Pin mattermost-notify action to its v2.1.0 commit SHA
The notify steps referenced mattermost/action-mattermost-notify@master, a
mutable branch that runs in CI with access to the MATTERMOST_WEBHOOK_URL
secret. Pinning to the immutable commit of the latest release (v2.1.0,
ae31bb6) keeps the exact reviewed code from executing, per GitHub third-party
action hardening guidance, while staying easy to bump.
2026-07-16 14:59:08 +02:00
Andrey Antukh 7f60e3735d 🐛 Remove hardcoded metadata URI default on webhook form (#10723)
The webhook creation form used a hardcoded AWS instance metadata
endpoint (http://169.254.169.254/...) as the default :uri value for
new webhooks. This leaked an internal cloud credential endpoint into
the UI defaults and could expose it to users.

Remove the default :uri so new webhooks start with an empty URI
instead of a sensitive hardcoded value.
2026-07-16 14:04:06 +02:00
Andrey Antukh 5cc6477e3f Merge remote-tracking branch 'origin/staging' into develop 2026-07-16 13:52:41 +02:00
Andrey Antukh 8e8fb67793 Merge remote-tracking branch 'origin/main' into staging 2026-07-16 13:52:28 +02:00
Andrey Antukh 258dd6ad6c 🐛 Handle text node targets in closest-text-editor-content (#10641)
Event targets can be DOM text nodes (nodeType 3) which lack the
.closest() method. Add a get-element helper that normalizes text
nodes to their parent Element before calling .closest(), matching
the existing pattern in dom/get-parent-with-data.

Fixes #10640

AI-assisted-by: mimo-v2.5-pro
2026-07-16 12:19:50 +02:00
David Barragán Merino 17c344b8f5 🐛 Fix auto-label action failing on PRs from forked repos 2026-07-16 10:15:22 +02:00
Elena Torro 001dba1bef Add background blur to text shapes 2026-07-16 08:33:36 +02:00
Andrey Antukh c50ec233ae Merge remote-tracking branch 'origin/staging' into develop 2026-07-15 21:09:12 +02:00
Andrey Antukh b3105e6b82 Backport github workflows from develop 2026-07-15 21:01:55 +02:00
Andrey Antukh e498dd2382 📎 Update commiter opencode agent 2026-07-15 21:01:33 +02:00
Andrey Antukh e48f374984 ⬆️ Update opencode on devenv dockerfile 2026-07-15 17:32:16 +02:00
María Valderrama bdc078d5ea 🐛 Fix mismatched subscription in social login (#10703) 2026-07-15 14:47:44 +02:00
María Valderrama 792d88dc4f 🐛 Fix invalid org invitation show toast (#10693)
* 🐛 Fix invalid org invitation show toast

* 📎 Code review
2026-07-15 12:30:20 +02:00
Alejandro Alonso ec4c5a75a7 Merge pull request #10697 from penpot/elenatorro-10537-fix-mask-position-within-layout
🐛 Fix masked group position in flex layout on child visibility change
2026-07-15 11:05:41 +02:00
Elena Torro 6285b1de60 🐛 Fix masked group position in flex layout on child visibility change 2026-07-15 08:58:48 +02:00
DoubleCat 2203482531 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 100.0% (2369 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-07-14 15:01:44 +02:00
María Valderrama 167aa7410f 🐛 Fix modals for nitrate subscription when unlimited (#10690) 2026-07-14 14:15:29 +02:00
Marina López c119622ad9 🐛 Changed color and text from nitrate banners 2026-07-14 12:59:16 +02:00
Pablo Alba 95cfbf5f7c Improve workspace debug sidebar workflow (#10678) 2026-07-14 12:42:19 +02:00
Elena Torró c11d3aaaa7 🐛 Add :stroke-image support to plugins API (#10683) 2026-07-14 10:26:19 +02:00
Elena Torró ab58d00d66 Improve bool intersection perfomance (#10671) 2026-07-14 07:43:52 +02:00
Andrey Antukh 85dbf14344 📎 Add better planner skill and improve testing doc 2026-07-13 11:42:26 +02:00
Andrey Antukh 33e18c72e2 📎 Add minor improvements to opencode setup 2026-07-11 10:12:07 +02:00
Elena Torró a006a12ab6 🐛 Fix render stroke caps on drag (#10634) 2026-07-10 12:33:59 +02:00
Dr. Dominik JainandClaude 2c15dcdb84 Add systematic component tests via a composable test model (#10529)
*  Add systematic component tests via a composable test model

Introduce a framework for systematically testing Penpot component behaviour
(synchronisation/propagation, swaps, variant switches, nesting), plus a first
suite of cases built on it.

A test is expressed as a COMPOSITION OF OPERATIONS over a "situation" (an
in-memory file value plus named role bindings). Operations are reified as data
and composed by two combinators — `in-sequence` (threads the situation) and
`one-of`/`optional` (alternatives, enumerated into concrete variants). So one
written case stands for a whole matrix of variants, and coverage grows by
composition rather than by copying tests. Operations drive the REAL production
change pipeline, and event-operations dispatch the REAL workspace events and
await settlement, so the production watcher's automatic propagation is what is
exercised — the tests reflect genuine app behaviour, not a reimplementation.

Structure (frontend/test/frontend_tests/composable_tests/):
  - core            — the domain-agnostic engine: situation, the operation and
                      enumeration protocols, the combinators, and the runners.
  - comp/nodes      — the component operations (create/instantiate/reset, nesting,
                      swap, the variant ops, child add/remove/move, change, undo,
                      library sync).
  - comp/setups     — component-shaped starting configurations.
  - interpreter     — runs a case against the real frontend store: sync-ops apply
                      directly, event-ops dispatch real events and await
                      settlement (absorbing sync-file's delayed status RPC, which
                      would otherwise leak an error into subsequent tests).
  - comp/sync-test  — the cases (B-F, H, I, K, L, M).

This is test-only code with a single consumer — the frontend test suite (the
layer that runs the real app) — so it lives entirely under the frontend test
tree as .cljs, not under app/common.

The framework and its cases are documented in the project memory
frontend/composable-component-tests, added alongside.

Co-authored-by: Claude <noreply@anthropic.com>

* 🐛 Guard WASM mock teardown against an empty snapshot

`teardown-wasm-mocks!` unconditionally restored from the `originals` atom.
When run without a matching setup (double teardown, or `with-wasm-mocks*`
misused around an async test body), the snapshot is empty and every WASM API
function was `set!` to nil — permanently, for the remainder of the test run.
Any later code calling one of them (e.g. a leaked debounced resize-wasm-text
event firing during a subsequent test namespace) then crashed with
"initialized? is not a function".

Make the restore a no-op when there is nothing to restore.

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 12:17:43 +02:00
Belén Albeza 176a813fb9 🐛 Fix selected text background color in light theme (#10614) 2026-07-10 12:02:30 +02:00
Andrey Antukh 3add7211a5 Merge remote-tracking branch 'origin/staging' 2026-07-10 11:35:04 +02:00
Andrey Antukh fd8cb957d1 Merge remote-tracking branch 'origin/staging' into develop 2026-07-10 11:23:42 +02:00
Andrey Antukh 1f4b85209e 📚 Simplify the ia asistance note on creating-prs serena workflow 2026-07-10 11:22:44 +02:00
AKandAndrey Antukh 89551b2415 🐛 Fix dashboard user menu submenu not closing on hover out (#10639)
The dashboard profile menu submenus (Help & Learning, Community &
Contributions, About Penpot) opened on pointer enter but nothing closed
them when the pointer left the option, leaving a stale submenu visible
until the whole menu closed.

Close the open submenu when the pointer leaves an expandable option or
its submenu, with a 200ms grace period (same approach as the workspace
context menu) so the submenu survives the pointer crossing the gap
between the parent menu and the floating submenu. Keyboard navigation
is unchanged and now covered by tests.

Closes #10549

AI-assisted-by: claude-fable-5

Signed-off-by: Akshit Nassa <nassaakshit@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-10 11:21:21 +02:00
Belén Albeza 3469867cf5 🐛 Fix text editor not auto-selecting all text on mount (#10573) 2026-07-10 11:08:10 +02:00
Eva Marco 3708cf31d4 🐛 Fix stroke cap selects (#10631) 2026-07-10 11:05:09 +02:00
Andrey Antukh e73aa9e981 Add PENPOT_INTERNAL_URI to exporter for separate internal and public URI handling (#10630)
Add PENPOT_INTERNAL_URI environment variable to the exporter. This allows
separating the URI used for internal communication (headless browser to
frontend) from the public URI used for resource references in exported SVGs.

Previously, PENPOT_PUBLIC_URI served both purposes, which caused exported
SVGs to contain broken font URLs when the internal Docker address was used.

Changes:
- Add :internal-uri to exporter config schema with fallback to :public-uri
- Add get-internal-uri helper function
- Use internal-uri for browser navigation in SVG/PDF/bitmap renderers
- Post-process SVG output to replace internal URI with public URI
- Use internal-uri for backend API calls in resource handler
- Log both URIs on startup
- Update docker-compose.yaml to use both variables
- Document the new variable in configuration.md

Closes #10627

AI-assisted-by: mimo-v2.5-pro
2026-07-10 10:41:49 +02:00
Alonso Torres 3ee5b50007 🐛 Fix problems with padding multiple values in plugins and UI (#10602)
* 🐛 Fix problem with padding types in plugins

* 🐛 Fix problem with multiple selection paddings
2026-07-10 09:13:20 +02:00
Whos DeezandAndrey Antukh 508569437a 📚 Fix flex word repetition and correct modifier key in docs (#10610)
* 📎 Update version on mcp package.json

* Update flexible-layouts.njk : fixed repetition and changed shortcut

Fixed 'Flex' word repetition; made the Toggle Flex instruction reflect the correct keyboard modifier key.

Signed-off-by: Whos Deez <134500894+Whosdeez@users.noreply.github.com>

* Update package version to 2.17.0

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Whos Deez <134500894+Whosdeez@users.noreply.github.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-10 09:01:43 +02:00
Andrey Antukh 1311bafec4 Merge remote-tracking branch 'origin/staging' into develop 2026-07-09 19:41:04 +02:00
Andrey Antukh ad4dae5f28 📚 Add testing principles to serena doc 2026-07-09 19:40:45 +02:00
Andrey Antukh f84a8687f6 Merge remote-tracking branch 'origin/staging' into develop 2026-07-09 19:37:16 +02:00
Andrey Antukh 23a9b4bdd9 🐛 Fix backend util shell tests 2026-07-09 19:34:15 +02:00
Andrey Antukh 3400d6afbb 🐛 Fix too much recursion when clicking shape in comments mode (#10622)
* 🐛 Fix too much recursion when clicking shape in comments mode

Remove `deselect-all` from `handle-interrupt` in comments mode. The
`select-shape` event emits `:interrupt` which the comments stream
watcher routes to `handle-interrupt`. In comments mode, calling
`deselect-all` cleared the selection and emitted a competing
`rt/nav`, creating a synchronous recursion cycle in the potok store
that overflowed the JS call stack.

Fixes #10620

AI-assisted-by: opencode

* 🐛 Add unit tests for comments handle-interrupt

Make `handle-interrupt` public (defn- → defn) and add 4 tests covering
each branch: draft thread, open thread, comments mode, and noop.

AI-assisted-by: opencode
2026-07-09 18:04:05 +02:00
Alonso Torres d6c50cc40b 🐛 Fix problems with plugins api setPluginData (#10632) 2026-07-09 18:00:26 +02:00
María Valderrama ebfcb716ac 🐛 Fix permission to add a team to an organization (#10623) 2026-07-09 14:12:41 +02:00
Andrey Antukh 64b0bff7bd 📚 Update serena creating-prs workflow documentation 2026-07-09 13:12:41 +02:00
Alonso Torres 80d688be93 🐛 Fix problems with comments clusters (#10543) 2026-07-09 11:37:45 +02:00
Andrey Antukh 28fd798c52 🐛 Fix crash with degenerate selrect in text pipeline (#10618)
Guard remaining text pipeline locations that accessed :selrect directly
against nil/zero-dimension selrects by using safe-size-rect, which
provides a 4-level fallback chain (selrect -> points -> shape fields ->
empty 0.01x0.01 rect).

Fixes:
- fix-position in viewport_texts_html.cljs: replaced dm/get-prop
  :selrect with ctm/safe-size-rect for both old and new shape
- assoc-position-data in modifiers.cljs: replaced (:selrect ...)
  with ctm/safe-size-rect for delta computation
- change-orientation-modifiers in modifiers.cljc: replaced raw
  :selrect access with safe-size-rect for scale and origin computation

Closes #10617

AI-assisted-by: mimo-v2.5-pro
2026-07-09 11:03:06 +02:00
Andrey Antukh 47a3158602 🐛 Fix component variant panel crash with mismatched property counts (#10616)
Use `get` instead of `nth` to avoid index-out-of-bounds when a selected
component copy has fewer variant properties than the first component in
the selection.

Closes #10615

AI-assisted-by: deepseek-v4-pro
2026-07-09 11:01:08 +02:00
Andrey Antukh 41ebb8e80b 🐛 Fix crash when converting SVG-raw shape to path (#10613)
Guard the content-to-PathData coercion on whether
stp/convert-to-path actually produced a new value, so
SVG-raw shapes (whose :content is a hiccup map) pass through
unchanged instead of crashing.

Closes #10612

AI-assisted-by: deepseek-v4-pro
2026-07-09 11:00:18 +02:00
Andrey Antukh f2460eee29 🐛 Fix Draft.js selection offset DOMException on text editor re-render (#10608)
Clamp selection offset to node length in setDraftEditorSelection to
prevent DOMException when Draft.js SelectionState offset exceeds the
actual DOM text node length. This can happen during spellcheck, IME
composition, or race conditions during React re-renders.

Updates @penpot/draft-js to commit 09a33e0a which includes the fix
in addPointToSelection and addFocusToSelection.

Fixes #10607

AI-assisted-by: mimo-v2.5-pro
2026-07-09 10:59:40 +02:00
Andrey Antukh 8e9df0c515 Add insert-multi chunked variant to app.db 2026-07-09 09:25:59 +02:00
Andrey Antukh 8fcd8a63b4 ⬆️ Update opencode dependency on devenv 2026-07-09 09:25:19 +02:00
Andrey Antukh 455c7f5cae 📚 Update serena doc related to creating issues 2026-07-09 08:48:00 +02:00
Andrey Antukh 54bef496f1 Merge remote-tracking branch 'origin/staging' into develop 2026-07-09 08:34:27 +02:00
Andrey Antukh b3d1a7aa8b 📚 Add better dev tools doc to serena 2026-07-09 08:33:46 +02:00
Andrey Antukh fb2ec4c617 Merge remote-tracking branch 'origin/staging' into develop 2026-07-09 08:14:15 +02:00
Andrey Antukh 10f240ce78 Merge remote-tracking branch 'origin/main' into staging 2026-07-09 08:13:18 +02:00
Elena Torró 8ac2e8c8a8 🐛 Fix sidebar scroll to shape (#10600) 2026-07-08 20:38:16 +02:00
Andrey Antukh a004f7cd84 Merge remote-tracking branch 'origin/staging' into develop 2026-07-08 15:44:37 +02:00
Andrey Antukh bcf77767b2 📖 Update ai agents documentation 2026-07-08 15:44:18 +02:00
María Valderrama 5d933fd770 🐛 Organizations list sorted by name (#10589) 2026-07-08 15:43:36 +02:00
Andrey Antukh 8b734d9844 📎 Add postgresql client tool wrapper for devenv 2026-07-08 14:22:34 +02:00
Pablo Alba cc454de9ce 🐛 Fix wrap nitrate sso when there is no profile (#10592) 2026-07-08 14:21:34 +02:00
Elena TorróandBelén Albeza d656d342fe 🐛 Fix text selrect size (#10566)
* 🐛 Fix text selrect size

* 🐛 Fix blinks on complex files

---------

Co-authored-by: Belén Albeza <belen@hey.com>
2026-07-08 13:56:58 +02:00
alonso.torres 88186eaec1 ⬆️ Release MCP 2.17.0 2026-07-08 12:57:45 +02:00
alonso.torres b153748866 ⬆️ Release plugins 1.5.0 2026-07-08 12:57:45 +02:00
alonso.torres f0cf8dca2a 🐛 Fix the release scripts 2026-07-08 12:57:45 +02:00
Andrey Antukh e61c55e53c 📚 Add no text-wrapping rule to the creating-issue workflow 2026-07-08 12:53:11 +02:00
Pablo Alba 0de8bce895 Add ignore sso flag to nitrate management api endpoints (#10579) 2026-07-08 12:51:03 +02:00
Andrey Antukh aadae99b91 📎 Add create-issue skill 2026-07-08 12:43:51 +02:00
Andrey Antukh 83ce70d02d 📎 Update version on mcp package.json 2026-07-08 12:07:40 +02:00
Andrey Antukh 1b00ca8cb9 Merge remote-tracking branch 'origin/staging' 2026-07-08 12:03:49 +02:00
Andrey Antukh fdb5291384 📚 Update changelog 2026-07-08 12:03:02 +02:00
Andrey Antukh d4aa512247 📚 Update changelog (add 2.18.0 section) 2026-07-08 11:57:27 +02:00
Dominik JainandMichael Panchenko b9e0421f79 📚 Update and improve devenv documentation
* Fix stale references to `run-devenv-agentic` and `start-devenv`
* Improve clarity in description of workspaces
* Improve section on agentic devenv

Co-authored-by: Michael Panchenko <michael.panchenko@oraios-ai.de>
2026-07-08 11:28:55 +02:00
Dominik Jain 93b5baa9fc 🐛 Relocate MCP run directory out of the npm-managed install directory
For the npm MCP package, adjust the launcher to specifically prepare
the runtime directory: The package contents are copied once per version
to a runtime directory owned by the launcher.

The npm-owned package directory cannot be used. While installing there
worked for older npm/npx version, we have to assume it is read-only.

Fixes #9947
2026-07-08 11:28:17 +02:00
Dominik Jain aee555ff6e 📚 Update API types for MCP server for new release 2026-07-08 11:27:46 +02:00
Andrey Antukh 0e25f7d8ad Merge remote-tracking branch 'origin/staging' into develop 2026-07-08 10:25:25 +02:00
Andrey Antukh 50a98c35bf 📎 Update creating-commits serena workflow 2026-07-08 10:22:40 +02:00
Andrey Antukh 1d97ae5b46 📎 Simplify create-pr skill 2026-07-08 10:22:40 +02:00
Eva Marco ff0d56a86e 🐛 Fix blur menu width (#10575) 2026-07-08 10:06:23 +02:00
Luis de Dios 4eaefec43e 🐛 Fix glitch when using drawing tool in toolbar (#10447) 2026-07-08 09:55:50 +02:00
Juan de la CruzandAndrey Antukh 8b16bb8874 Add new slides content for 2.17 release (#10577)
*  Add new slides content for 2.17 release

* 📎 Fix linter issues

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-08 09:39:38 +02:00
Alonso Torres 7655f854a4 Improved api test suite (#10533)
* 🐛 Fix plugins api issues

* 🐛 Fix problem with text weight

* 🐛 Fix api problems

*  Updated plugins test suite

* 🐛 Referential integrity tests

*  Add resetOverrides method

* 🐛 Fix plugins components structure poluting
2026-07-07 16:12:12 +02:00
Aitor Moreno f51db39b8d Merge pull request #10563 from penpot/ladybenko-gh-10467-fix-autowidth
🐛 Fix autowidth in text editor v3
2026-07-07 14:24:19 +02:00
Belén Albeza a58c0e1407 🐛 Fix autowidth in text editor v3 2026-07-07 13:20:03 +02:00
Marina López 3e2826de9e Redesign my penpot to my files 2026-07-07 12:22:58 +02:00
Juanfran ac31edab14 🐛 Skip end-user SSO gate on nitrate org management endpoints (#10559)
get-teams-detail, get-org-invitations and delete-org-invitations lacked
::rpc/auth false, so wrap-nitrate-sso ran on them whenever params carried
an organization-id. For SSO-active orgs this rejected the org owner's
admin-console reads with a 401, since their Penpot session has no SSO
grant for the org. These are shared-key protected management calls made
on the org owner's behalf and never use profile-id, so they should not
require an end-user SSO session — matching their sibling endpoints.
2026-07-07 12:22:17 +02:00
Belén Albeza 0a2570c9eb 🐛 Fix text editor not repainting text if there was not a geometry change (#10553) 2026-07-07 11:18:44 +02:00
Dr. Dominik Jain c88d9f568b 🐛 Preserve component order when combining components as variants (#10562) 2026-07-06 16:49:29 +02:00
Belén Albeza 83d2edf256 🐛 Fix empty text shape not being removed (#10541) 2026-07-06 15:42:42 +02:00
Alonso Torres 2dfb1046f7 🐛 Fix problem with rotations crashing (#10560) 2026-07-06 11:56:46 +02:00
Luis de Dios e5d3ea14d1 🐛 Fix size badge shows "x" instead of dimensions when creating a path (#10550) 2026-07-03 14:58:43 +02:00
Andrey Antukh 618ec57b6f 📎 Update changelog 2026-07-03 11:10:53 +02:00
Andrey Antukh a4ba5fc2e0 Merge remote-tracking branch 'origin/staging' into develop 2026-07-03 10:41:21 +02:00
Andrey Antukh 1228b9aa08 Merge remote-tracking branch 'origin/main' into staging 2026-07-03 10:41:01 +02:00
Andrey Antukh 62a9053422 📎 Add minor agents and skills restructuration for opencode 2026-07-03 10:39:42 +02:00
Marina López 04f6549e82 🐛 Replace hyphens with bullets in subscriptions benefits 2026-07-03 10:35:23 +02:00
Andrey Antukh 50c0299e7f ⬆️ Update devenv dockerfile 2026-07-03 09:49:31 +02:00
Juanfran 0182f239ae Add Nitrate bulk profile creation endpoint (#10491)
Add a Nitrate-only management RPC method to create active Penpot profiles in
bulk through the existing shared-key protected management API.
2026-07-03 09:45:44 +02:00
Luis de Dios 2c4a9e0f82 Improve dashboard invitations modal (#10459) 2026-07-03 09:28:52 +02:00
Eva Marco 841b47736b 🐛 Fix token pill border color on invalid pills when not selected (#10535)
Signed-off-by: Eva Marco <eva.marco@kaleidos.net>
2026-07-03 09:28:27 +02:00
Eva Marcoandsxxtony 0387cdf7e4 ♻️ Migrate shared ancillary components to modern syntax (#10527)
* ♻️ Migrate shared ancillary components to modern syntax

* ♻️ Address shared ancillary syntax review

* ♻️ Apply Step 3 props destructuring on numeric-input*

Replaces the legacy ?-suffixed prop aliases with clean :keys destructuring
and updates internal references, addressing review on PR #9406.

* 🐛 Fix some problems on refactored components

---------

Co-authored-by: sxxtony <166789813+sxxtony@users.noreply.github.com>
2026-07-03 09:26:16 +02:00
Belén Albeza 2e32a8743e ♻️ Refactor rulers into ui modules (#10461)
* ♻️ Move rulers rendering to ui submodule

* ♻️ Refactor RulerState into UIState
2026-07-03 08:37:22 +02:00
Luis de Dios d795a1f110 Manage rotation and color of selection size badge (#10393)
*  Rotate and move size badge when shape is rotated

* 🐛 Fix wrong text color in selection size badge

*  Add tests

* ♻️ Extract common constants
2026-07-02 14:59:33 +02:00
Alejandro Alonso b82ab0c830 Merge remote-tracking branch 'origin/staging' into develop 2026-07-02 13:52:25 +02:00
alonso.torres f1426cb3bb 🐛 Add test of fixes in the plugins api suite 2026-07-02 13:32:17 +02:00
RenzoMXDandAlonso Torres 1c9ab691e6 🐛 Plugin API theme.addSet/removeSet accept proxy or set ID
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>

🐛 Preserve validation errors for theme set APIs

Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>

Co-authored-by: Alonso Torres <alonso.torres@kaleidos.net>
2026-07-02 13:32:17 +02:00
Filip SajdakandClaude Opus 4.8 5b7447fbe4 🐛 Accept delay 0 in plugin API interaction setter
The InteractionProxy `delay` setter rejected `0` via `(not (pos? value))`,
so an immediate after-delay interaction (`interaction.delay = 0`) was
refused. The model allows it: `set-delay`
(common/.../shape/interactions.cljc) only asserts `check-safe-int`, with
no positivity constraint, and `safe-int` permits 0.

Replace the guard with `(or (not (sm/valid-safe-int? value)) (neg? value))`:
it allows 0 and positive integers, rejects negatives, and rejects
non-integers cleanly (the previous `number?` check let fractional values
through, which then failed the model's `check-safe-int` assertion
downstream).

Adds a regression test (delay round-trips a positive value, then 0).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-07-02 13:32:17 +02:00
Filip SajdakandClaude Opus 4.8 1c5c6ee072 🐛 Accept fractional border radius in plugin API shape setters
The ShapeProxy `borderRadius` setters (`borderRadius` and the four
per-corner variants) validated with `sm/valid-safe-int?`, so a fractional
radius (e.g. `shape.borderRadius = 7.5`) was rejected as invalid. The data
model types `:r1`-`:r4` as `::sm/safe-number` (shape.cljc), the radius
sidebar input only constrains `min 0` (not integer), and `set-radius-*`
store the value verbatim — so the plugin API was stricter than the model.
Same class of defect as the merged #9780.

Switch those 5 guards to `sm/valid-safe-number?`, keeping the existing
non-negative guard on the all-corners setter. Integer-only setters in the
file (`getRange` bounds, `setParentIndex`) keep `valid-safe-int?`.

Adds a regression test asserting fractional radius values are accepted
(with throwValidationErrors enabled).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-07-02 13:32:17 +02:00
Filip SajdakandClaude Opus 4.8 c7ee54e849 🐛 Accept fractional gap and padding in plugin API layout setters
The flex and grid layout proxies validated `rowGap`, `columnGap` and the
four padding setters with `sm/valid-safe-int?`, so a fractional value
(e.g. `flex.rowGap = 10.5`) was rejected as invalid. The data model types
`:row-gap`/`:column-gap` and `:p1`-`:p4` as `::sm/safe-number`
(layout.cljc), and the sidebar accepts decimals, so the plugin API was
stricter than the model — the same class of defect as the merged #9780.

Switch those 16 gap/padding guards (8 in flex.cljs, 8 in grid.cljs) to
`sm/valid-safe-number?`, matching the model and the predicate already used
by the flex-element setters in the same file. Integer-only setters
(`zIndex`, grid track indices/counts and cell positions/spans) keep
`valid-safe-int?`. Also fixes a `:righPadding` typo in two grid
rightPadding error branches.

Adds a regression test asserting fractional gap/padding values are
accepted (with throwValidationErrors enabled) for both flex and grid.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-07-02 13:32:17 +02:00
mvanhorn 355f7acb66 🐛 Make animation optional in plugin CloseOverlay type and align overlay typings 2026-07-02 13:32:17 +02:00
mvanhorn ab623cc02d 🐛 Make overlay position optional in plugin open-overlay/toggle-overlay interactions 2026-07-02 13:32:17 +02:00
Filip SajdakandClaude Opus 4.8 76070008c2 🐛 Fix plugin API board.guides invalid-schema on set/clear
Setting or clearing `board.guides` from a plugin threw a malli
`:malli.core/invalid-schema` for every value, including `[]`. Causes:

- `shape.cljs` validated against `[:vector ::ctg/grid]`, but the
  `:app.common.types.grid/grid` reference is no longer registered
  (direct schemas replaced the namespaced-keyword registrations). Use
  the direct var `ctg/schema:grid`, matching every other setter.
- `parser.cljs` `parse-frame-guide` returned the `parse-frame-guide-column`
  / `parse-frame-guide-row` fns instead of calling them with the guide,
  so column/row guides parsed to a vector containing a function.
- `parse-frame-guide-square` used `parse-frame-guide-column-params`
  instead of the dedicated `parse-frame-guide-square-params`.

Adds a regression test parsing column/square guides and validating the
result (and an empty vector) against `ctg/schema:grid`.

Fixes #9773

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-07-02 13:32:17 +02:00
Filip SajdakandClaude Opus 4.8 8fe835a9cc 🐛 Surface real plugin errors instead of bare "Value not valid"
The plugin proxy `:on-error` handler funneled every throwable through
`handle-error`, which only produced a message for malli ExceptionInfo
carrying `::sm/explain`. Two paths lost the real cause and rendered the
unhelpful "[PENPOT PLUGIN] Value not valid. Code: :error":

- A plain JS error (TypeError, etc.) is not an ExceptionInfo, so
  `(ex-data cause)` is nil and the message bound to nil; the real
  `.-message` only reached the host-page console, invisible to the
  plugin sandbox.
- A malli explain whose errors flatten to nothing made `error-messages`
  return "", which rendered as "Value not valid: . Code: :error".

`error-messages` now returns nil (not "") when there is nothing to
render, and `handle-error` falls back to the raw explain, then to the
throwable's `ex-message`/`str`, so a useful message always surfaces.
Working cases (well-formed explain) are unchanged.

Adds regression tests for the plain-JS-error path, the empty-explain
path, and the `error-messages` nil contract.

Fixes #9692

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-07-02 13:32:17 +02:00
Eva Marco f1f5a30624 🐛 Fix alignment selector for strokes (#10532) 2026-07-02 13:01:58 +02:00
andrés gonzález 40a79c2b5d 📚 Update user guide for 2.17 release (#10518) 2026-07-02 09:06:44 +02:00
Alexis Morin 30e17eb076 🌐 Add translations for: French (Canada)
Currently translated at 99.0% (2347 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-07-02 03:01:19 +02:00
Stephan Paternotte 40091924e2 🌐 Add translations for: Dutch
Currently translated at 99.7% (2364 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-07-02 03:01:18 +02:00
DoubleCat b3585428bd 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 79.4% (1882 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-07-02 03:01:18 +02:00
Belén Albeza 6d458c80a1 🐛 Fix font and variant dropdowns on mixed text styles (#10520)
* 🐛 Fix dropdown shown Mixed Font Families for same family with different variant

* 🐛 Fix variants dropdown appearing blank on mixed variants but same family

*  Add playwright test for mixed font families/variants
2026-07-01 20:15:20 +02:00
Elena Torró 28b5371901 🔧 Improve text strokes in texts with emoji (#10519) 2026-07-01 18:10:46 +02:00
Alejandro Alonso f25317ac47 🐛 Fix text inner stroke fill seam by compositing fill inside SrcIn (#10509)
* 🐛 Fix text inner stroke fill seam by compositing fill inside SrcIn

* 🔧 Update e2e Playwright screenshots
2026-07-01 16:24:23 +02:00
Andrey Antukh 6cc2c403c4 ⬆️ Update dependencies 2026-07-01 14:45:41 +02:00
Marina López 64ba70e6f3 🐛 Fix border color for selected subscription 2026-07-01 14:42:06 +02:00
Andrey Antukh 44d33a0c80 Merge remote-tracking branch 'origin/staging' into staging 2026-07-01 14:41:01 +02:00
Andrey Antukh 77a8677b60 Merge remote-tracking branch 'origin/main' into staging 2026-07-01 14:40:36 +02:00
Eva Marco e6a341ff30 🐛 Fix text layout item menu (#10516) 2026-07-01 14:39:34 +02:00
Andrey Antukh 1006efc571 🐛 Fix translation strings 2026-07-01 14:38:08 +02:00
Eva Marco 04254c9fd1 🐛 Fix token tooltips (#10515) 2026-07-01 14:30:18 +02:00
Aitor Moreno 851fde01c1 🐛 Fix SVG Raw not being saved (#10492) 2026-07-01 14:08:54 +02:00
Eva Marco 665e8e018e 🐛 Fix overlay position grid (#10512) 2026-07-01 13:40:12 +02:00
Eva Marco abbd28d101 🐛 Fix long text inside color tooltip (#10511) 2026-07-01 13:38:51 +02:00
Luis de Dios 7e8590b04c 🐛 Fix inspect in workspace displays shape size twice when selecting a shape (#10507) 2026-07-01 13:08:35 +02:00
Belén Albeza 215de966ae 🐛 Fix extra linebreak on MacOS IME (#10498)
* 🐛 Fix extra linebreak in Japense IME (MacOS)

* ♻️ Remove composing? from the state and query event instead
2026-07-01 12:40:25 +02:00
Elena Torró c8f586a197 🐛 Fix async text rendering on tiles (#10504) 2026-07-01 11:09:57 +02:00
Belén Albeza 51877c4bca 🐛 Fix text editor cursor being too thin on dpr > 1 (#10501) 2026-07-01 10:43:59 +02:00
Alejandro Alonso 05e9c68a74 🐛 Fix preserving empty text fill when editing without changes (#10483) 2026-06-30 17:59:54 +02:00
Eva Marco 920c6d4e76 🐛 Fix token tooltips on numeric-input, color and typography (#10480) 2026-06-30 16:26:49 +02:00
Andrey Antukh 782adffec8 Merge remote-tracking branch 'origin/staging' into develop 2026-06-30 16:26:26 +02:00
Andrey Antukh 8087f2e42b 📎 Update .nvmrc 2026-06-30 16:25:24 +02:00
Andrey Antukh c4e72fd7f9 Merge remote-tracking branch 'origin/staging' into develop 2026-06-30 16:23:23 +02:00
Andrey Antukh 88961c6fd3 🔧 Add recursive install to plugins test workflow 2026-06-30 16:15:30 +02:00
Andrey Antukh 5bae6bd078 📎 Fix mcp deprecation warning 2026-06-30 16:15:30 +02:00
Andrey Antukh 1c02810b83 📎 Fix fmt issues on plugins 2026-06-30 16:15:30 +02:00
Andrey Antukh 92035ebd94 ⬆️ Update and fixed warnings on plugins module 2026-06-30 16:15:30 +02:00
Andrey Antukh f38d6cf0f2 ⬆️ Update nodejs version across docker images 2026-06-30 16:15:30 +02:00
Andrey Antukh e2b52d88fc ⬆️ Update dependencies 2026-06-30 16:15:30 +02:00
ParinithandAndrey Antukh 8d5b16295f 💄 Center libraries empty state placeholder vertically (#10452)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-30 16:14:24 +02:00
Luis de Dios 28afe223f4 🐛 Invitations to a team can be sent to existing members without displaying any error (#10489)
* 🐛 Fix team invitations can be sent to existing members without displaying any error

*  Add tests
2026-06-30 15:39:59 +02:00
Alonso Torres 8823f7ac4d Make v2 plugins default throw on error (#10433) 2026-06-30 14:50:50 +02:00
Luis de Dios 46f5346045 ♻️ Merge :thumbnails and :thumbnails-meta into single state key (#10021)
* ♻️ Merge :thumbnails and :thumbnails-meta into single state key

♻️ Unify thumbnail refs in a single ref

🐛 Fix test

* ♻️ Update tests
2026-06-30 14:37:27 +02:00
Andrew Barnes b3b3ea97db 📚 Fix Angular plugin usage doc link (#10349) 2026-06-30 14:35:55 +02:00
Alonso Torres e0be9f7ade Plugin for api testing (#10410) 2026-06-30 14:01:38 +02:00
Belén Albeza e6a49adfbc 🐛 Fix crash on composition update when pressing Esc on a IME (#10479) 2026-06-30 13:55:06 +02:00
Alonso Torres ca81776d04 🐛 Fix problems when dragging frame with comments (#10460)
* 🐛 Fix undo frame position not undoing comments

* 🐛 Fix problem with hover capturing dragging event

* 🐛 Fix watch updates for comment bubbles

* 🐛 Fix "Maximum update depth" crash on SVG shape transforms

* 🐛 Fix comment geometry problems
2026-06-30 13:33:47 +02:00
Elena Torró 21f646aeee 🐛 Fix decimal rounding and format in WebGL rulers (#10487) 2026-06-30 10:35:03 +02:00
VKing9 557a267176 🌐 Add translations for: Hindi
Currently translated at 83.8% (1987 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-06-30 07:01:22 +02:00
AntonPalmqvist f05fb70a8a 🌐 Add translations for: Swedish
Currently translated at 99.7% (2364 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-30 07:01:19 +02:00
Alonso Torres f993f203bd 🐛 Fix problems with plugins API (#10412)
*  Adds static dispatch safe stubs in tests

* 🐛 Fix shapesColors metadata key to match ColorShapeInfo

* 🐛 Fix CommentThread.remove rejecting the owner's own threads

* 🐛 Fix page.removeCommentThread throwing on a spurious Promise

*  Implement ShapeBase.swapComponent in the plugin API

*  Expose File.revn in the plugin API

* 🐛 Fix FileVersion.createdAt calling Luxon method on a js/Date

* 🐛 Fix plugin font/typography application to text and ranges

* 🐛 Default plugin overlay interaction position for non-manual types

* 🐛 Fix plugin interaction setters passing an id-only shape

* 🐛 Fix grid addColumnAtIndex rejecting valid track types

* 🐛 Expose libraryId on library color/typography/component proxies

*  Implement LibraryTypography.setFont in the plugin API

* 🐛 Fix typography.applyToTextRange reading unexposed range bounds

* 🐛 Fix utils.geometry.center argument mismatch

* 🐛 Fix localStorage.removeItem calling getItem

* 🐛 Fix shape backgroundBlur proxy key casing

* 🐛 Report boolean shape type as 'boolean' in the plugin API

* 🐛 Return the resulting paths from plugin flatten

* 🐛 Make plugin z-order methods act on the target shape

* 🐛 Make is-variant-container? return a boolean

*  Implement Group.isMask in the plugin API

* 🐛 Return a shape proxy from TextRange.shape

* 🐛 Return the duplicated set from TokenSet.duplicate

* 🐛 Fix theme addSet/removeSet reading set name with a keyword

* 🐛 Accept string fontFamilies token value in the plugin API

* 🐛 Fix combineAsVariants ignoring the passed component ids

* 🐛 Fix board removeRulerGuide ignoring its argument

* 🐛 Fix board guides setter schema and parser

* 🐛 Avoid 0-byte allocation when syncing empty grid tracks

* 🐛 Validate grid track indices in the plugin API

* 🐛 Return null for empty input in group() and centerShapes()

* 🐛 Return TokenTypographyValue[] from a typography token's resolvedValue

* 🐛 Return TokenShadowValue[] from a shadow token's resolvedValue

* 🐛 Return string[] from a fontFamilies token's resolvedValue

* 🐛 Clear mutually-exclusive reps when setting LibraryColor gradient/image

* 🐛 Add readonly tags to types, deprecate Image type

* 📚 Update plugins changelog
2026-06-29 17:32:15 +02:00
Alejandro Alonso 99638fa60c 🐛 Fix wasm render playwright tests (#10462)
* 🐛 Fix playwright wasm test for updating canvas background

* 🐛 Fix playwright wasm test for rendering blurs

* 🐛 Fix invisible emoji texts in render-wasm playwright data
2026-06-29 17:03:45 +02:00
Aitor MorenoandElena Torró 2d4a24bf97 🐛 Fix stroke to path extra points (#10190)
* 🐛 Fix stroke to path extra points

* 🐛 Set evenodd when needed on stroke to path (#10446)

---------

Co-authored-by: Elena Torró <elenatorro@gmail.com>
2026-06-29 10:34:44 +02:00
Pablo Alba a9f3949abc Avoid going to last team on login if it is protected by sso (#10442) 2026-06-29 09:44:16 +02:00
Alonso Torres a47b0122c7 🐛 Fix problem with measures menu (#10445) 2026-06-29 09:24:37 +02:00
DoubleCat 785c5ffdfc 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 79.4% (1881 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-06-28 12:01:25 +02:00
Stephan Paternotte 3369abd988 🌐 Add translations for: Dutch
Currently translated at 99.7% (2364 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-27 09:01:22 +02:00
Belén Albeza e9c0982a94 Disable hot swap of render engines (#10444) 2026-06-26 15:04:52 +02:00
Andrey Antukh 5212e2202b Merge remote-tracking branch 'origin/staging' into develop 2026-06-26 14:34:00 +02:00
Andrey Antukh a1352fc79e Merge remote-tracking branch 'origin/main' into staging 2026-06-26 14:33:45 +02:00
Andrey Antukh 0dee6e3cb0 📚 Add let binding algnment info to serena 2026-06-26 14:32:32 +02:00
Andrey Antukh 925dca35ab 📚 Update common testing doc on .serena 2026-06-26 14:28:49 +02:00
Andrey Antukh 736a22ab1d 📚 Add paren-repair script for automatic parentheses repair 2026-06-26 14:28:15 +02:00
Alejandro Alonso 44e39a1008 🐛 Sync WASM viewport when locating board in grid layout editor (#10443) 2026-06-26 14:24:44 +02:00
Eva Marco 6a79383082 🐛 Blur info doesn't show on inspect in certain shapes (#10427)
* 🐛 Blur info doesn't show on inspect in certain shapes

* 🎉 Add test
2026-06-26 14:10:41 +02:00
Belén Albeza 10147b6abd 🐛 Fix pixel grid and board pixel grid shown on top of rulers (#10430)
* 🐛 Fix pixel grid shown on top of rulers

* 🐛 Fix board pixel grid being rendered above rulers
2026-06-26 11:53:11 +02:00
Luis de Dios 8e9fb91959 🐛 Fix view mode is not persisted in color picker (#10369) 2026-06-26 11:38:51 +02:00
Pablo Alba 6e61e3304b Add and endpoint for nitrate to check the SSO configuration for an organization (#10432) 2026-06-26 11:38:18 +02:00
Elena Torró 89f882ecda 🐛 Fix viewer rendering on Firefox+NVIDIA setup (#10385) 2026-06-26 10:59:22 +02:00
Luis de Dios d16a2c93e0 🐛 Fix long typography token name in tooltip in design tab (#10387) 2026-06-26 10:50:19 +02:00
Luis de Dios 66719a14f5 🐛 Fix assets typography container is longer than others (#10406)
* 🐛 Fix assets typography container is longer than others

* ♻️ Use new SCSS guidelines
2026-06-26 09:42:22 +02:00
Denys Kisil f8974288e7 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 87.1% (2064 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-06-25 23:01:20 +02:00
Alejandro Alonso 345affc687 🐛 Fix premature WASM view-interaction end during pan (#10425) 2026-06-25 15:07:37 +02:00
Elena Torró 67386a0358 🐛 Fix missing tiles on the left (#10421) 2026-06-25 13:30:53 +02:00
Juanfran d328cb4a9e Enable org owners to view organization teams (#10388) 2026-06-25 13:07:20 +02:00
David Barragán Merino 14fb211733 🐛 Set org as owner for auto-label workflow 2026-06-25 12:41:18 +02:00
David Barragán Merino ce1191d86f 🐛 fix missing app-id in auto-label workflow 2026-06-25 11:53:07 +02:00
Belén Albeza d323aa9693 🐛 Fix guides not clipping (#10423) 2026-06-25 11:50:58 +02:00
Andrey Antukh 8645801eed 📎 Update changelog 2026-06-25 11:33:59 +02:00
David Barragán Merino f996ef372d 🔧 Migrate auto-label workflow from PAT to GitHub App toke 2026-06-25 11:26:15 +02:00
Andrey Antukh adbb5a8b5f Merge remote-tracking branch 'origin/staging' into develop 2026-06-25 10:44:07 +02:00
Andrey Antukh a668702982 Merge remote-tracking branch 'origin/main' into staging 2026-06-25 10:43:33 +02:00
Andrés Moya 4d1706f71c 🐛 Fix error when sending user feedback form (#10419)
If the smtp-default-from is something like "Penpot
<no-reply@penpot.app>", the schema validation fails because
it expects a simple email without < >
2026-06-25 10:37:50 +02:00
Pablo Alba 2a5b6a69ad Send warning for email about nitrate orgs with sso (#10413) 2026-06-25 09:53:07 +02:00
Andrey Antukh 2eb9423963 Merge remote-tracking branch 'origin/staging' into develop 2026-06-25 09:33:15 +02:00
Andrey Antukh f50d8edb13 Merge remote-tracking branch 'origin/main' into staging 2026-06-25 09:32:49 +02:00
María Valderrama 15a336a249 Allow nitrate to view org teams (#10365)
*  Allow nitrate to view org teams

* 📎 Code review

* 📎 Code review 2
2026-06-25 09:28:23 +02:00
Alonso Torres 28f3b8048a Improve MCP handling when tab is frozen in the browser (#10392) 2026-06-25 08:27:33 +02:00
David Barragán Merino d58d816310 🔧 Define concurrency policy to cancel in progress build workflows (#10409) 2026-06-25 08:22:41 +02:00
Elena Torró 149caaf292 🐛 Fix rulers values visibility (#10408) 2026-06-25 07:20:41 +02:00
Jack StormentandLuis de Dios aedb7f9195 Add dedicated Line and Arrow drawing tools (#9146)
*  Add dedicated Line and Arrow drawing tools

Introduce a Line/Arrow toolbar option and a click-drag drawing
interaction that matches Figma's workflow: select the tool, press and
drag to define the line in one gesture, with Shift snapping to 15°
increments. Arrowhead style can be toggled on either endpoint via the
existing stroke-cap controls.

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 💄 Fix formatting error

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 🐛 Translate line and arrow tooltips in top toolbar

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* 🐛 Add missing namespace

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* 📚 Update copyright notice

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* Add translations (EN) for toolbar elements

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* Add translations (ES) for toolbar elements

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* ♻️ Improve stroke-cap-end update for arrow handling

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* 🐛 Fix shortcuts select tool but do not replace it in the toolbar

Refactor tool selection logic in top_toolbar.cljs

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

* ♻️ Remove unnecessary blank line

Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>

---------

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com>
Signed-off-by: Luis de Dios <luis.dedios@kaleidos.net>
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-06-24 21:32:13 +02:00
Alejandro Alonso b096832bf5 🐛 Fix v2 text editor detaching typography tokens (#10402) 2026-06-24 18:20:21 +02:00
Aitor Moreno 888f6798cf Merge pull request #10407 from penpot/superalex-render-from-cache-4
🐛 Fix render from cache
2026-06-24 16:32:26 +02:00
Alejandro Alonso 9b22d96553 🐛 Fix render from cache 2026-06-24 14:00:57 +02:00
Miguel de Benito DelgadoandAndrey Antukh a6c7bd28e8 💄 Fix some malli schemas (#7733)
* 🐛 Add missing fields to schema:profile

* 🐛 Add missing fields to schema:dissolve-animation

* 📎 Add minor changes

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-24 13:16:25 +02:00
Stephan Paternotte b9136e6fda 🌐 Add translations for: Dutch
Currently translated at 87.0% (2062 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-24 13:01:25 +02:00
Andrey Antukh 3036ef473e Merge remote-tracking branch 'origin/staging' into develop 2026-06-24 11:31:03 +02:00
Andrey Antukh 4687b52bf3 📎 Fix fmt issues introduced in previous commits 2026-06-24 11:30:32 +02:00
Andrey Antukh 673c87cf33 Merge remote-tracking branch 'origin/staging' into develop 2026-06-24 11:18:23 +02:00
Andrey Antukh bfa65547f8 Add more testing related improvements 2026-06-24 11:17:47 +02:00
Andrey Antukh 403e1ec604 Merge remote-tracking branch 'origin/staging' into develop 2026-06-24 11:03:18 +02:00
Andrey Antukh 0d3a174f13 Merge remote-tracking branch 'origin/main' into staging 2026-06-24 11:03:05 +02:00
Andrey Antukh 27274a56fd 📎 Update changelog 2026-06-24 11:01:38 +02:00
Andrey Antukh 9391535f48 📎 Add minor enhancement to update-changelog skill 2026-06-24 11:01:14 +02:00
Dr. Dominik Jain 0270c2a90f 🐛 Fix description of TokenCatalog.addTheme in high-level overview (#10359)
Fixes #10074
2026-06-24 10:27:28 +02:00
Luis de Dios 5ef8d35683 🐛 Fix avoid flashing the move tool before activating the selected tool (#10291) 2026-06-24 10:25:37 +02:00
DexterityandAndrey Antukh a530cf0dec ♻️ Migrate render-wasm api object-svg to modern component syntax (#9459)
* ♻️ Migrate render-wasm api object-svg to modern component syntax

* 📎 Add minor changes

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-24 10:24:53 +02:00
DexterityandAndrey Antukh d757f96633 ♻️ Migrate svg filter components to modern syntax (#9448)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-24 10:21:18 +02:00
Pablo Alba 8fa15f240f Check for nitrate sso on move team to another organization (#10379) 2026-06-24 10:02:13 +02:00
Andrey Antukh fa9012e55f Merge remote-tracking branch 'origin/staging' into develop 2026-06-23 15:28:42 +02:00
Eva Marco 1a9a831e72 🎉 Activate by default token combobox flag (#10378) 2026-06-23 15:16:51 +02:00
Andrey Antukh 06e6671813 Merge remote-tracking branch 'origin/staging' into develop 2026-06-23 12:31:25 +02:00
Andrey Antukh 9259b596dc Merge remote-tracking branch 'origin/main' into staging 2026-06-23 12:26:17 +02:00
Andrés Moya 5042a34e3c 🔧 Normalize text nodes comparison, to be used in tokens detach
🔧 Add more tests for all cases and fix text token application in tests
2026-06-23 12:24:30 +02:00
Stephan Paternotte 1a88864cbb 🌐 Add translations for: Dutch
Currently translated at 86.6% (2053 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-23 10:01:33 +00:00
Yaron Shahrabani 30be0cae71 🌐 Add translations for: Hebrew
Currently translated at 85.0% (2016 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-06-23 10:01:30 +00:00
Oğuz Ersen cf4fb8d4a2 🌐 Add translations for: Turkish
Currently translated at 99.7% (2364 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-06-23 10:01:27 +00:00
Ingrid Pigueron af83a0c7f7 🌐 Add translations for: French
Currently translated at 84.8% (2010 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-23 10:01:21 +00:00
Andrey Antukh 1b3a3b4cdb 📎 Add auto-label and auto-project github workflow 2026-06-23 11:27:38 +02:00
Andrey Antukh f967a0fc83 Add improvements for frontend tests (#10380) 2026-06-23 11:21:53 +02:00
Ricardo Sawir 121c76235f 🐛 Fix zh-CN boolean intersection label (#10381)
Update the Simplified Chinese translation for the boolean intersection menu item so it no longer duplicates the difference label.
2026-06-23 11:12:05 +02:00
Elena Torró 07de0e92d5 Fix slow zoom/edit on Firefox+NVIDIA WebGL renderer (#10371) 2026-06-23 10:58:14 +02:00
Juanfran fc07a6467b 🐛 Fix organizations dropdown clipped by sidebar overflow (#10362)
Render the organizations selector dropdown in a portal anchored to the
trigger button, so a long list is no longer clipped by the
sidebar-content-wrapper overflow.
2026-06-23 10:37:59 +02:00
Elena Torró 8e548c8c54 🐛 Fix blank tiles and atlas crash on render-wasm zoom/pan (#10367)
* 🐛 Fix missing tiles on page switch and pan/zoom end

* 🐛 Fix blank tiles and atlas crash on render-wasm zoom/pan
2026-06-23 10:25:45 +02:00
since-2017-hubandAndrey Antukh 46abf1c968 ♻️ Migrate auth flow pages to modern component syntax (#9469)
Refactor the 6 mf/defc components across the auth flow
(login, password recovery, recovery request) to modern
mf/defc name* syntax. Modern syntax avoids per-render
JS->CLJS prop-conversion overhead.

- Rename login-page -> login-page* in
  frontend/src/app/main/ui/auth/login.cljs.
- Rename 
ecovery-form -> 
ecovery-form* and
  
ecovery-page -> 
ecovery-page* in
  frontend/src/app/main/ui/auth/recovery.cljs.
- Rename 
ecovery-form -> 
ecovery-form*,
  
ecovery-request-page -> 
ecovery-request-page*,
  
ecovery-sent-page -> 
ecovery-sent-page* in
  frontend/src/app/main/ui/auth/recovery_request.cljs
  (drop redundant {::mf/props :obj} marker since the *
  suffix already implies it).
- Drop unused :as props destructure bindings.
- Update internal forwarding callsites (2) and external
  callers in auth.cljs (3 callsites + 3 :refer imports),
  viewer/login.cljs (1 callsite + 1 :refer), and
  static.cljs (2 callsites + 1 :refer).

Refs #9260

Signed-off-by: since-2017-hub <since2017hub@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-23 09:38:07 +02:00
Elena Torró dd353a8121 🔧 Update design-tab tests for default background-blur flag 2026-06-23 09:28:28 +02:00
Filip SajdakandClaude Opus 4.8 66c8ebf198 🐛 Accept negative letterSpacing in plugin API text setters
The plugin text API rejected negative letter-spacing even though the
product UI allows -200..200 (typography.cljs). Two defects in
frontend/src/app/plugins/text.cljs:

- `letter-spacing-re` (`#"^\d*\.?\d*$"`) had no provision for a leading
  minus, so any negative value failed validation.
- The text-range `:letterSpacing` setter inverted its guard: it used
  `(or (empty? value) (re-matches ...))` to mean "invalid", which
  rejected matching values and let non-numeric input through. The
  text-shape setter and the sibling `lineHeight` range setter both
  correctly use `(not (re-matches ...))`.

Fix the regex to allow an optional leading minus and add the missing
`not` so the range setter matches the shape setter. Adds regression
coverage for the regex accept/reject contract.

Fixes #9780

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Filip Sajdak <filip.sajdak@siili.com>
2026-06-22 22:40:44 +02:00
DexterityandAndrey Antukh ea20291d2a ♻️ Migrate v1 text-editor to modern component syntax (#9446)
* ♻️ Migrate v1 text-editor to modern component syntax

* 📎 Add minor changes

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-22 18:40:01 +02:00
Krishna zadeandAndrey Antukh 0dbc2c54d6 🐛 Fix spacebar activating pan mode in editable fields as Comment Input box (#10287)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-22 17:32:12 +02:00
Alejandro Alonso e9410dce6b Merge pull request #10368 from penpot/elenatorro-fix-guides-pill-on-drag
🐛 Fix guides pill on drag
2026-06-22 15:58:34 +02:00
Elena Torro 20e90078db 🐛 Fix guides pill on drag 2026-06-22 15:40:12 +02:00
Andrey Antukh 4c5991514a 🐛 Fix syntax issues introduced in prev commit 2026-06-22 15:36:42 +02:00
Aitor Moreno a0d9603243 Merge pull request #10340 from penpot/hiru-fix-text-change-detection
🔧 Normalize text nodes comparison, to be used in tokens detach
2026-06-22 15:33:04 +02:00
DexterityandAndrey Antukh 22cf4917d8 ♻️ Migrate shapes/export components to modern syntax (#9449)
* ♻️ Migrate shapes/export components to modern syntax

* 📎 Add minor changes

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Remove whitespace

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-22 15:28:00 +02:00
Luis de Dios aec56be9f5 🐛 Fix inspect in View Mode displays shape size twice when selecting a shape (#10364) 2026-06-22 14:46:48 +02:00
Eva Marco 3aa46379a1 🎉 Activate background-blur flag by default (#10366) 2026-06-22 14:46:01 +02:00
Andrey Antukh 4bbf9a6617 Merge remote-tracking branch 'origin/staging' into develop 2026-06-22 14:40:40 +02:00
Andrey Antukh 623a80ca00 🔧 Expose storybook port on devenv
Using 3451 port instead of the previous 6006
2026-06-22 14:35:10 +02:00
Andrey Antukh d8434cbffb 🐛 Add missing migrations (#10363) 2026-06-22 13:26:21 +02:00
Eva Marco 4e33ce7c46 🐛 Fix very long token names on remap modal (#10356) 2026-06-22 13:12:41 +02:00
Eva Marco e495e0ac59 🐛 Allow negative value on margins (#10353) 2026-06-22 13:12:30 +02:00
Andrey Antukh 5a82a38c9c 📎 Update changelog 2026-06-22 12:55:51 +02:00
Andrés Moya f2bf5a3111 🔧 Add more tests for all cases and fix text token application in tests 2026-06-22 12:54:40 +02:00
Alejandro Alonso 3a9be0b1d8 Merge pull request #10358 from penpot/niwinz-bugfix-1
🐛 Fix incorrect events handling on webgl render toggle
2026-06-22 12:54:13 +02:00
Andrey Antukh 3b9a895f62 🐛 Fix incorrect events handling on webgl render toggle
From the workspace main menu
2026-06-22 11:29:57 +02:00
Luis de Dios 7c19ace0f0 Reapply "🎉 Add flyout and semantic improvements to main toolbar (#9480)" (#10354)
This reverts commit 94119159d8.
2026-06-22 10:39:29 +02:00
Marina López e8e0d68019 Changed org avatar shape from rounded to squared 2026-06-22 09:59:43 +02:00
Andrey Antukh 19a851aacb Merge branch 'develop' into staging 2026-06-22 09:52:28 +02:00
Andrey Antukh a627d5c4d1 Merge remote-tracking branch 'origin/staging' into develop 2026-06-22 09:52:06 +02:00
Luis de Dios a74aa10dc1 🐛 Fix correction of some margins, alignment and ensuring consistency (#10351) 2026-06-22 09:49:33 +02:00
Andrey Antukh e3325dd3eb 🌐 Validate and rehash translation files 2026-06-22 09:33:10 +02:00
Alexis Morin 12fa25edf1 🌐 Add translations for: French (Canada)
Currently translated at 98.1% (2326 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:30:44 +02:00
VKing9 aeb63765c6 🌐 Add translations for: Hindi
Currently translated at 83.1% (1969 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hi/
2026-06-22 09:30:43 +02:00
Henrik Allberg 823bef44e0 🌐 Add translations for: Swedish
Currently translated at 98.3% (2330 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:30:42 +02:00
Црнобог 9bd0cd47a7 🌐 Add translations for: Serbian
Currently translated at 56.6% (1342 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sr/
2026-06-22 09:30:41 +02:00
Alejandro Alonso c9144a18fb 🌐 Add translations for: Yoruba
Currently translated at 48.4% (1148 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/yo/
2026-06-22 09:30:39 +02:00
Alejandro Alonso a5cb53fcdf 🌐 Add translations for: Igbo
Currently translated at 20.6% (490 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ig/
2026-06-22 09:30:38 +02:00
Revenant afd2b75a25 🌐 Add translations for: Malay
Currently translated at 27.2% (646 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ms/
2026-06-22 09:30:38 +02:00
Alejandro Alonso 50f6ce1c0a 🌐 Add translations for: Hausa
Currently translated at 51.2% (1214 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ha/
2026-06-22 09:30:37 +02:00
Sebastiaan Pasma af415dbc4d 🌐 Add translations for: Dutch
Currently translated at 85.1% (2017 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-22 09:30:36 +02:00
Stephan Paternotte 5953e1d48b 🌐 Add translations for: Dutch
Currently translated at 85.1% (2017 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/nl/
2026-06-22 09:30:35 +02:00
Edgars Andersons 98084f55b3 🌐 Add translations for: Latvian
Currently translated at 77.9% (1846 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/lv/
2026-06-22 09:30:34 +02:00
deveronica 117600fc7a 🌐 Add translations for: Korean
Currently translated at 83.9% (1989 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ko/
2026-06-22 09:30:33 +02:00
Denys Kisil eb006d2bf2 🌐 Add translations for: Ukrainian (ukr_UA)
Currently translated at 84.4% (2001 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ukr_UA/
2026-06-22 09:30:31 +02:00
Zvonimir Juranko 44f26b93a8 🌐 Add translations for: Croatian
Currently translated at 66.2% (1570 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2026-06-22 09:30:30 +02:00
al0cam aad8ec280e 🌐 Add translations for: Croatian
Currently translated at 66.2% (1570 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/hr/
2026-06-22 09:30:29 +02:00
Dário 6609018842 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 65.0% (1542 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2026-06-22 09:30:28 +02:00
TheScientistPT b644563e2a 🌐 Add translations for: Portuguese (Portugal)
Currently translated at 65.0% (1542 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_PT/
2026-06-22 09:30:27 +02:00
Amerey.eu dbc13f4cc4 🌐 Add translations for: Czech
Currently translated at 66.0% (1565 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/cs/
2026-06-22 09:30:26 +02:00
Mikel Larreategi 7c87e65577 🌐 Add translations for: Basque
Currently translated at 48.7% (1155 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/eu/
2026-06-22 09:30:24 +02:00
Radek Sawicki 3e7d52a709 🌐 Add translations for: Polish
Currently translated at 47.6% (1130 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pl/
2026-06-22 09:30:23 +02:00
Jacopo Lodovico Trabia 26291aa007 🌐 Add translations for: Italian
Currently translated at 89.1% (2111 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-22 09:30:23 +02:00
Nicola Bortoletto 09a00edeb6 🌐 Add translations for: Italian
Currently translated at 89.1% (2111 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-22 09:30:22 +02:00
william chen 8cc38f2ba9 🌐 Add translations for: Chinese (Traditional Han script)
Currently translated at 66.2% (1569 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hant/
2026-06-22 09:30:21 +02:00
Yaron Shahrabani f2f98d43d3 🌐 Add translations for: Hebrew
Currently translated at 84.9% (2013 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-06-22 09:30:19 +02:00
Linerly f2400ab5df 🌐 Add translations for: Indonesian
Currently translated at 70.4% (1668 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/id/
2026-06-22 09:30:18 +02:00
Youkho c6c288033a 🌐 Add translations for: Arabic
Currently translated at 47.3% (1121 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ar/
2026-06-22 09:30:17 +02:00
AlexTECPlayz 41ac7e3ec3 🌐 Add translations for: Romanian
Currently translated at 80.6% (1911 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ro/
2026-06-22 09:30:12 +02:00
Stas Haas 4f1eca2500 🌐 Add translations for: German
Currently translated at 82.2% (1948 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/de/
2026-06-22 09:30:11 +02:00
Renan Mayrinck 3b47d88111 🌐 Add translations for: Portuguese (Brazil)
Currently translated at 58.1% (1377 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/pt_BR/
2026-06-22 09:30:10 +02:00
Jun Fang d9a255f6de 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 74.8% (1773 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-06-22 09:30:09 +02:00
Beeby Xia 0679f2164f 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 74.8% (1773 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-06-22 09:30:08 +02:00
IsCycleBai f590485c77 🌐 Add translations for: Chinese (Simplified Han script)
Currently translated at 74.8% (1773 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/zh_Hans/
2026-06-22 09:30:07 +02:00
Oğuz Ersen ba16d28be1 🌐 Add translations for: Turkish
Currently translated at 98.3% (2330 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-06-22 09:30:06 +02:00
The_BadUser a99ef52311 🌐 Add translations for: Russian
Currently translated at 70.3% (1666 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-06-22 09:30:04 +02:00
Vin ffbbb56a5f 🌐 Add translations for: Russian
Currently translated at 70.3% (1666 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-06-22 09:30:03 +02:00
Egor Filatov 21657f100d 🌐 Add translations for: Russian
Currently translated at 70.3% (1666 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ru/
2026-06-22 09:30:02 +02:00
Unreal Vision 057478ddf1 🌐 Add translations for: French
Currently translated at 84.5% (2003 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-22 09:30:01 +02:00
GradelerM 4bc57619d8 🌐 Add translations for: French
Currently translated at 84.5% (2003 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-22 09:30:00 +02:00
Corentin Noël 45f9f3e08f 🌐 Add translations for: French
Currently translated at 84.5% (2003 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-06-22 09:29:59 +02:00
Anonymous f4a643e099 🌐 Add translations for: Spanish
Currently translated at 95.3% (2259 of 2369 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/es/
2026-06-22 09:29:58 +02:00
Hosted Weblate 1952de884d 🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2026-06-22 09:25:31 +02:00
Alexis Morin c087a45424 🌐 Add translations for: French (Canada)
Currently translated at 99.6% (2330 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Alexis Morin 40247c6d6e 🌐 Add translations for: French (Canada)
Currently translated at 96.1% (2248 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Alexis Morin 8eb20f1b05 🌐 Add translations for: French (Canada)
Currently translated at 93.0% (2177 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Alexis Morin 796f95348a 🌐 Add translations for: French (Canada)
Currently translated at 90.8% (2126 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Nicola Bortoletto ad963831f7 🌐 Add translations for: Italian
Currently translated at 90.4% (2115 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-22 09:25:28 +02:00
AntonPalmqvist eb71b05d29 🌐 Add translations for: Swedish
Currently translated at 99.7% (2334 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:28 +02:00
Alexis Morin c42c2f6e84 🌐 Add translations for: French (Canada)
Currently translated at 88.5% (2072 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
AntonPalmqvist 66dcff290c 🌐 Add translations for: Swedish
Currently translated at 97.0% (2270 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:28 +02:00
Joseph V M ef8ddaca4f 🌐 Add translations for: Malayalam
Currently translated at 2.9% (69 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ml/
2026-06-22 09:25:28 +02:00
Alexis Morin 532f8410ab 🌐 Add translations for: French (Canada)
Currently translated at 88.1% (2062 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Oğuz Ersen 6f58bffd83 🌐 Add translations for: Turkish
Currently translated at 99.7% (2334 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-06-22 09:25:28 +02:00
AntonPalmqvist 26def5424e 🌐 Add translations for: Swedish
Currently translated at 96.6% (2260 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:28 +02:00
Alexis Morin 92ff7f779a 🌐 Add translations for: French (Canada)
Currently translated at 87.3% (2044 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
AntonPalmqvist d93e7157cd 🌐 Add translations for: Swedish
Currently translated at 93.6% (2191 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:28 +02:00
Alexis Morin ef6b1ff9a6 🌐 Add translations for: French (Canada)
Currently translated at 86.4% (2023 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
Oğuz Ersen 57073da5b9 🌐 Add translations for: Turkish
Currently translated at 97.6% (2283 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/tr/
2026-06-22 09:25:28 +02:00
Alexis Morin b8a2c5d34e 🌐 Add translations for: French (Canada)
Currently translated at 85.5% (2001 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-06-22 09:25:28 +02:00
AntonPalmqvist 84151bd62b 🌐 Add translations for: Swedish
Currently translated at 87.8% (2054 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:28 +02:00
Nicola Bortoletto d5eee48c68 🌐 Add translations for: Italian
Currently translated at 89.9% (2105 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/it/
2026-06-22 09:25:27 +02:00
AntonPalmqvist c14ca30707 🌐 Add translations for: Swedish
Currently translated at 86.3% (2019 of 2339 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/sv/
2026-06-22 09:25:27 +02:00
Andrey Antukh 960b90253f ⬆️ Update root deps 2026-06-22 09:04:13 +02:00
Andrey Antukh d8894abcbc 📎 Update changelog 2026-06-22 09:04:01 +02:00
Andrés Moya 5f8d9740d6 🔧 Normalize text nodes comparison, to be used in tokens detach 2026-06-19 14:58:20 +02:00
Belén Albeza 5775e947ad 🐛 Fix page blur disappearing early 2026-06-19 13:41:42 +02:00
Andrey Antukh 2c5aaaa3c6 Revert "🐛 Highlight first matching font when searching the font picker (#9512)"
This reverts commit aba6e214ed.
2026-06-19 13:25:09 +02:00
Belén Albeza c4115a6143 🐛 Fix eyedropper not taking into account changes in dpr 2026-06-19 13:16:49 +02:00
Elena Torró b91eece5bf 🐛 Fix rulers shown on thumbnails 2026-06-19 13:01:26 +02:00
Alejandro Alonso b9ebe9f4ee 🐛 Fix viewer tile artifacts 2026-06-19 12:51:17 +02:00
Alejandro Alonso 6bc94ad6c6 Merge pull request #10333 from penpot/ladybenko-gh-10321-fix-guides-hover
🐛 Fix hover bugs in wasm guides
2026-06-19 12:30:56 +02:00
Eva Marco 59f9f2e163 🐛 Fix position of font selector on font family token modal 2026-06-19 12:10:47 +02:00
Pablo Alba b984e7bbe8 Add nitrate sso wards to organization navigation 2026-06-19 12:03:21 +02:00
Belén Albeza 2fbff5816c 🐛 Fix hover bugs in wasm guides 2026-06-19 11:47:33 +02:00
Krishna zadeandAndrey Antukh 08721127a3 🐛 Fix incorrect color count in color libraries dropdown (#10281)
* 🐛 Fix incorrect color count in color libraries dropdown

* 📎 Add minor formatting changes

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-19 11:32:14 +02:00
9e52bb40d0 Add process-level resource limits to font processing tools (#10274)
*  Add font processing resource limits via prlimit

Font processing tools (fontforge, sfnt2woff, woff2sfnt, woff2_decompress)
were invoked via clojure.java.shell/sh with no timeouts or resource limits.
This adds process-level resource limits using prlimit(1) and the shell/exec!
infrastructure from the ImageMagick hardening work.

shell/exec! changes:
- Add :prlimit parameter that prepends prlimit(1) to the command
- :prlimit takes {:mem <MiB> :cpu <seconds>} for address space and CPU time
  limits, enforced by the kernel's RLIMIT subsystem
- prlimit-cmd builds the prlimit command prefix (private helper)

Font processing changes:
- Replace all clojure.java.shell/sh calls with shell/exec! via exec-font!
- exec-font! applies font-prlimit (512 MiB, 30s CPU, 60s wall-clock)
- All 5 conversion functions (ttf->otf, otf->ttf, ttf-or-otf->woff,
  woff->sfnt, woff2->sfnt) use try/finally for explicit temp file cleanup
- Remove clojure.java.shell require from media.clj

Tests:
- Add exec-prlimit-normal, exec-prlimit-cpu, exec-prlimit-memory tests

Closes #10234

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

*  Make font processing resource limits configurable

Replace hardcoded font-prlimit map and wall-clock timeout with
config-driven values under the PENPOT_FONT_PROCESS_* namespace.
The prlimit implementation detail is not exposed in config keys.

Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>

---------

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>
Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>
2026-06-19 11:30:48 +02:00
RenzoandAndrey Antukh aba6e214ed 🐛 Highlight first matching font when searching the font picker (#9512)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-19 11:29:45 +02:00
David Barragán Merino 1b84655fb4 📚 Add the emoji code to Commit types table 2026-06-19 11:24:09 +02:00
Alejandro Alonso 2a5c29421f 🐛 Cap GPU max texture size at 4096 2026-06-19 09:38:38 +02:00
Andrey Antukh d3bec95860 🐛 Allow pasting comma-separated emails in multi-input (#10186)
The multi-input component did not handle paste events for
comma-separated values. When users pasted emails like
'qa@example.com, test@example.com', the entire string was
inserted as-is, triggering validation errors.

The on-key-down handler already split text on commas/spaces
when typing, but paste events bypassed this logic.

Added an on-paste handler that:
- Detects if pasted text contains commas or whitespace
- Splits the text by commas and/or whitespace
- Validates each part individually
- Adds valid items to the items list
- Prevents default paste behavior
- Resets input state after processing

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-19 09:38:00 +02:00
andrés gonzálezandCursor 564cd1b528 Show and manage comments while designing in the workspace (#10275)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 09:30:56 +02:00
0xRapzzand0xRapzz 1c8d26faaf 🐛 Fix swapped tooltip messages for token deletion states (#10316)
The tooltip messages for 'not-active' and 'has-errors' conditions
were swapped in both the typography row and color row components.

When a token is deleted (not-active), the tooltip should show the
'deleted-token' message, and when a referenced token has errors
(has-errors), it should show the 'not-active-token' message.

Fixes #10296
Fixes #10299

Signed-off-by: 0xRapzz <oxrapzz@rapzzclip.win>
Co-authored-by: 0xRapzz <oxrapzz@proton.me>
2026-06-19 08:18:25 +02:00
Pablo Alba 038ab5e1f7 🐛 Fix go to your penpot on error page (#10322) 2026-06-19 01:06:36 +02:00
Alonso Torres bbf63e1136 🐛 Fix array format in plugins properties (#10246) 2026-06-19 00:59:47 +02:00
Alonso Torres ecabe7ec32 🐛 Fix token creation fail when set inactive (#10297)
* 🐛 Fix token creatin fail when set inactive

* 🎉 Add a enable flag to addSet to enable the token set
2026-06-19 00:58:45 +02:00
Luis de Dios 4a41b2e5e0 ♻️ Update in-app onboarding slides (#10086)
* ♻️ Modify social media icons in verification email

* ♻️ Update verify email

* ♻️ Update copies in 'check your email'

* ♻️ Update onboarding images

* ♻️ Refurbish create team slide

* ♻️ Refactor SCSS for in-app onboarding

* 🐛 Fix replace old uxbox with penpot image for all email HTMLs

* 🐛 Fix use of link component
2026-06-18 22:49:29 +02:00
Alejandro Alonso c6ecfb7794 Merge pull request #10320 from penpot/superalex-fix-performance-regression-3
🐛 Fix performance regression
2026-06-18 18:37:40 +02:00
Alejandro Alonso 01fc3c3e7d 🐛 Defer tile atlas composition to full frames only 2026-06-18 18:33:10 +02:00
Alejandro Alonso f6f716de3a 🐛 Fix performance regression 2026-06-18 18:30:51 +02:00
Miguel de Benito Delgado 93b9f5c567 🐛 Follow 302 redirects when downloading templates (#10308)
* 💄 Update URIs for templates to avoid redirects

* 🐛 Follow 302 redirects when downloading templates
2026-06-18 18:14:56 +02:00
196e47fa93 Backport MCP related changes from develop (#10315)
* ♻️ Add mcp integration state management refactor (#10226)

* ♻️ Add mcp integration state management refactor

* 🐛 Fix access tokens do not appear

* ♻️ Refactor some names

* ♻️ Refactor token deletion

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>

* 🐛 Fix stale MCP token data after create/regenerate (#10280)

Fix the root cause in profile.cljs: remove the optimistic conj from
access-token-created and instead chain a fetch-access-tokens after the
create-access-token API call succeeds. This ensures all callers get a
fresh, server-consistent token list automatically.

Suggested-by: niwinz

Signed-off-by: kapilvus <kapil69265@gmail.com>
Co-authored-by: kapilvus <kapilvus@gmail.com>

*  Remove non-recoverable mcp key warning from regenerated modal (#10298)

---------

Signed-off-by: kapilvus <kapil69265@gmail.com>
Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
Co-authored-by: kapil971390 <kapil69265@gmail.com>
Co-authored-by: kapilvus <kapilvus@gmail.com>
2026-06-18 18:08:40 +02:00
David Barragán MerinoandAndrey Antukh 235f1137f1 ⬆️ Updgrade base image for penpot docker images to ubuntu 26.04 (#10031)
* ⬆️ Updgrade base image for penpot docker images to ubuntu 26.04

* ⬆️ Update playwright

* 🐳 Use dist-upgrade to update all system packages

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-18 18:07:46 +02:00
Alonso Torres 75e23cb9a3 🐛 Fix problem with plugins creating interactions always added a new flow (#10231) 2026-06-18 18:00:19 +02:00
Alonso Torres 6b50e2d822 🐛 Fix errors with code generation (#10217)
* 🐛 Fix errors with code generation

* 🐛 Add cancelation of effects in inspect code
2026-06-18 17:55:55 +02:00
Andrey Antukhandmimo-v2.5-pro b4532486e3 Add configurable resource usage limits for imagemagick (#10240)
* 🐳 Add ImageMagick policy.xml resource limits to backend Docker image

Add a restrictive policy.xml to the backend Docker image that caps
ImageMagick resource usage: 256MiB memory, 512MiB map, 128MP area,
30s time limit, 16KP max dimensions. Blocks PS/EPS/PDF/XPS coders
to prevent Ghostscript attack surface.

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

*  Add timeout support to shell/exec!

Add optional :timeout parameter (in seconds) that uses
Process.waitFor(long, TimeUnit). On timeout, the process is
destroyed forcibly and an :internal/:process-timeout exception
is raised. Stdout/stderr readers handle IOException from closed
streams when the process is killed.

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* ♻️ Rename ::wrk/netty-executor to ::wrk/executor with cached pool

Replace DefaultEventExecutorGroup (fixed Netty thread pool) with a
cached thread pool (px/cached-executor) for general async task
offloading. The cached pool creates threads on demand and reuses
idle ones, which is more appropriate for blocking I/O workloads
(shell commands, message bus, rate limiting, etc.).

Changes:
- Rename ::wrk/netty-executor to ::wrk/executor in worker/executor.clj
- Switch implementation from DefaultEventExecutorGroup to px/cached-executor
- Update all ig/ref wiring in main.clj (msgbus, tmp cleaner, climit, rlimit, rpc)
- Remove ::wrk/netty-executor from redis.clj (let lettuce create its own
  eventExecutorGroup instead of sharing a Netty executor)
- Assert executor is present in shell/exec! to prevent silent nil usage
- Remove executor-threads config (no longer needed for cached pool)

The ::wrk/netty-io-executor (NioEventLoopGroup) remains unchanged as it
handles actual non-blocking network I/O for Redis and S3.

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 🔥 Remove im4java dependency and replace with direct ImageMagick CLI calls

- Replace im4java Java library with direct 'magick' CLI calls via shell/exec!
- Add PENPOT_IMAGEMAGICK_* config env vars for resource limits (thread, memory, map, area, disk, time, width, height)
- Use configurable ImageMagick environment with sensible defaults matching policy.xml
- Remove -Dim4java.useV7=true JVM flag from startup scripts
- Remove org.im4java/im4java from deps.edn
- All ImageMagick commands now use shell/exec! with 60s timeout and resource limits

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 💄 Rename imagemagick env functions and optimize config reads

- Rename imagemagick-defaults -> imagemagick-default-env
- Rename imagemagick-env -> get-imagemagick-env
- Optimize to avoid double cf/get calls per config key

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

*  Add tests for shell/exec! timeout and media processing

- Add shell_test.clj: tests for exec! timeout, env vars, stdin, stderr
- Add media_test.clj: tests for info, generic-thumbnail, profile-thumbnail
- Fix generic-process to prefer explicit format over input mtype
- Fix shell/exec! to use cached executor when system has no executor
- Fix reduce-kv accumulator in set-env (must return penv)

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* ♻️ Refactor media/process to take system as first argument

- Change (defmulti process :cmd) -> (defmulti process (fn [_system params] (:cmd params)))
- Change (run params) -> (run system params)
- All process methods now receive [system params]
- Update all callers: rpc/commands/media, profile, auth, fonts
- Revert shell/exec! to require system with executor (no fallback)
- Fix lint warnings and formatting

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 🔥 Remove unused app.svgo namespace

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 🔥 Remove Node.js from backend Docker image

- Delete unused svgo-cli.js script
- Remove Node.js installation from Dockerfile.backend
- Remove svgo-cli.js copy from backend build script

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 🔥 Remove unused process-error multimethod

- Remove process-error multimethod and its default handler
- Simplify media/run to directly call process
- Fix alignment in main.clj

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

* 📚 Add ImageMagick resource limits configuration to technical guide

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>

---------

Co-authored-by: mimo-v2.5-pro <mimo-v2.5-pro@penpot.app>
2026-06-18 17:52:01 +02:00
Alejandro Alonso fe942f9780 Enable viewer WASM when render-wasm/v1 is active (#10314) 2026-06-18 17:47:46 +02:00
Andres Gonzalez 27c6761597 🐛 Emit create-shape-layout telemetry when adding grid layout via plugins or MCP 2026-06-18 16:38:00 +02:00
alonso.torres f81d4af05e 🐛 Changes after review 2026-06-18 16:35:07 +02:00
alonso.torres 823f2107cd 🐛 Add create variant util function 2026-06-18 16:35:07 +02:00
alonso.torres 445d14293e 🐛 Fix issue with padding and margin tokens in plugins 2026-06-18 16:18:46 +02:00
Alejandro Alonso 6f558bad2a Merge pull request #10312 from penpot/elenatorro-load-rulers-fast
 Improve rulers loading time
2026-06-18 16:12:36 +02:00
Eva Marco 955da2a9c2 🐛 Fix circular reference error on token edition (#10185)
* 🐛 Fix circular reference error on token edition

* ♻️ Move the fn to the helpers page

* 🎉 Add comment
2026-06-18 15:46:07 +02:00
Alonso Torres 0ad2864ebe 🐛 Fix problem with flow starting board (#10244) 2026-06-18 15:35:06 +02:00
Alonso Torres 5eb9753278 🐛 Fix problem with empty strings on createText plugins method (#10219) 2026-06-18 15:30:53 +02:00
Alonso Torres 68dd8ecdf5 🐛 Add fixedWhenScrolling to API (#10218) 2026-06-18 15:29:47 +02:00
Elena Torro bfef6ea089 Improve rulers loading time 2026-06-18 14:38:30 +02:00
alonso.torres b9dfa0c607 🐛 Fix problem when adding a variant children 2026-06-18 14:24:40 +02:00
Eva Marco 390a031099 🐛 Fix background blur on frame shapes 2026-06-18 14:23:14 +02:00
Eva Marco 9805d97e45 🐛 Fix font-selector position (#10302) 2026-06-18 13:36:15 +02:00
Andrey Antukh 0aca418007 Remove non-recoverable mcp key warning from regenerated modal (#10298) 2026-06-18 13:26:16 +02:00
Andrey Antukh 94119159d8 Revert "🎉 Add flyout and semantic improvements to main toolbar (#9480)"
This reverts commit 9a3023e5d0.
2026-06-18 12:37:23 +02:00
Belén Albeza d56c9f7bf6 🐛 Fix color picker (wasm) reading colors with disordered bytes 2026-06-18 12:11:06 +02:00
Marina López 785ab53f8c Cancel subscription when user deletes account 2026-06-18 12:09:12 +02:00
Alejandro Alonso 3d2a5a2957 Merge pull request #10216 from penpot/ladybenko-gh-10213-fix-double-click-guide
🐛 Fix double click not editing the guide
2026-06-18 12:06:13 +02:00
Andrey Antukh 540bc97787 🐛 Remove inconsistent library :is-indirect handling on frontend state
Related to #9506
2026-06-18 11:29:21 +02:00
kapil971390andkapilvus 11f3ef2549 🐛 Fix stale MCP token data after create/regenerate (#10280)
Fix the root cause in profile.cljs: remove the optimistic conj from
access-token-created and instead chain a fetch-access-tokens after the
create-access-token API call succeeds. This ensures all callers get a
fresh, server-consistent token list automatically.

Suggested-by: niwinz

Signed-off-by: kapilvus <kapil69265@gmail.com>
Co-authored-by: kapilvus <kapilvus@gmail.com>
2026-06-18 11:26:50 +02:00
Alonso Torres b573a71017 🐛 Fix numeric values for tokens (#10270) 2026-06-18 11:05:14 +02:00
Alonso Torres a7e57c78cf 🐛 Add validation for current page on plugins API (#10271) 2026-06-18 11:04:35 +02:00
Andrey Antukhanddeepseek-v4-flash 18c8769f05 ♻️ Extract wait-for-persistence into shared helper (#10272)
Add wait-persisted and force-persist-and-wait to app.main.data.persistence,
removing 5 inline copies and 2 private helper functions across the codebase.

Replaced in:
- assets.cljs       -> dwp/force-persist-and-wait 400
- clipboard.cljs    -> dps/force-persist-and-wait 400
- versions.cljs     -> dwp/wait-persisted (3 call sites, dropped 2 priv fns)
- shape.cljs        -> dwp/wait-persisted 5000

Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>
2026-06-18 10:43:30 +02:00
Andrey Antukh 203817fe6a Merge remote-tracking branch 'origin/staging' into develop 2026-06-18 09:44:10 +02:00
Andrey Antukhanddeepseek-v4-flash 71f5c11a11 Qualify MCP Redis channel names with tenant prefix
Read PENPOT_TENANT env var (defaulting to "default") and embed it in
Redis Pub/Sub channel names as penpot.mcp.<tenant>.task.{req,res}.<id>.

This prevents cross-tenant interference when multiple environments share
a Redis instance, matching the backend convention
(e.g. penpot.rlimit.<tenant>.window.<name> in app.rpc.rlimit).

Co-authored-by: deepseek-v4-flash <deepseek-v4-flash@penpot.app>
2026-06-18 09:22:29 +02:00
andrés gonzález fb8587ed3f 🐛 Fix register modal heading copy (Sign up for free) (#10263) 2026-06-18 09:12:25 +02:00
XavijuandXavier Julian 9a3023e5d0 🎉 Add flyout and semantic improvements to main toolbar (#9480)
Co-authored-by: Xavier Julian <xavier.julian@kaleidos.net>
2026-06-17 21:29:38 +02:00
Eva Marco 895c9cb8da 🐛 Fix tokens fonts combobox to show resolved value 2026-06-17 21:18:08 +02:00
Andrey Antukh a2e69db265 ⬆️ Update deps 2026-06-17 21:18:08 +02:00
Andrey Antukh 18a77953a7 Merge remote-tracking branch 'origin/staging' into develop 2026-06-17 19:13:32 +02:00
Andrey Antukh fe9598f96c Merge remote-tracking branch 'origin/main' into staging 2026-06-17 19:13:16 +02:00
Andrey Antukh 594bbf9dd6 📎 Update pr and commits worflow on serena 2026-06-17 19:12:26 +02:00
Elena Torró 80abc3fe3d 🐛 Fix shape fill flickering from color picker (#10273) 2026-06-17 18:41:41 +02:00
Alonso Torres 0afa108804 🐛 Fix stacked backdrop blurs 2026-06-17 17:44:40 +02:00
Andrey Antukh 2defd5c155 ⬆️ Upgrade docs/ dependencies and migrate to elevently v3 (#10242)
- Convert .eleventy.js to eleventy.config.mjs (ESM) since
  @11ty/eleventy-plugin-rss@3.0.0 is ESM-only
- Replace search-index.json.njk with search-index.json.11ty.js
  to avoid async templateContent access in Nunjucks filters
- Update feed.njk to use new RSS plugin v3 filter names:
  rssLastUpdatedDate -> getNewestCollectionItemDate | dateToRfc3339
  rssDate -> dateToRfc3339
- Add 11ty.js to templateFormats for search index generation
2026-06-17 17:37:20 +02:00
Alonso Torres c783260265 🐛 Fix problem with export and fonts (#10238) 2026-06-17 16:24:50 +02:00
David Barragán Merino 25484f53e7 🔧 Fix pnpm-workspace settings to allow the installation of wrangler (#10241) 2026-06-17 16:14:02 +02:00
Alejandro AlonsoandElena Torro 61cd9fe886 🐛 Fix performance regression
* 🔧 Preserve atlas on zoom interaction 

Co-authored-by: Elena Torro <elenatorro@gmail.com>
2026-06-17 14:52:12 +02:00
Eva Marco bdc9b092c5 Add proper props checking to several workspace sidebar components (#10159)
*  Add memo to sidebar components

*  Add memo to layout-container component

*  Add memo to layout-item component

*  Add memo to constraits component

*  Add memo to stroke-menu component

*  Add memo to shadows-menu component

*  Add memo to blur-menu component

*  Add memo to frame-grid-menu component

*  Add memo to grid-cell/options component

*  Add memo to svg-attrs component

*  Add check props to text-menu component

* 🐛 Fix CI
2026-06-17 14:39:03 +02:00
Alejandro Alonso 585d6944fc Merge pull request #10243 from penpot/elenatorro-fix-docatlas-cap
🐛 Fix DocAtlas cap
2026-06-17 10:42:48 +02:00
Elena Torro 82cd11a1ad 🐛 Fix DocAtlas cap 2026-06-17 09:52:57 +02:00
Eva Marco 8b20a3da15 🐛 Fix replace text by ref when dropdown is opened by click (#10174)
* 🐛 Fix replace text by ref when dropdown is opened by click

* 🎉 Add test
2026-06-17 08:39:40 +02:00
Andrey Antukh 0a54533240 Merge remote-tracking branch 'origin/staging' into develop 2026-06-17 00:11:23 +02:00
Andrey Antukh cc4cdff729 🔥 Remove duplicated github workflow 2026-06-17 00:05:07 +02:00
Andrey Antukh b645f51486 Backport .github directory from develop 2026-06-17 00:03:25 +02:00
Andrey Antukh 0338655cd0 📎 Add frontend pnpm-lock.yaml dedup 2026-06-17 00:02:07 +02:00
Andrey Antukh c9f9bd5029 📎 Use same playwright version on all frontend subpackages 2026-06-16 23:22:19 +02:00
Eva Marcoandalonso.torres 2a098e5b16 🎉 Add background blur (#10034)
* 🎉 Add background blur

* 🎉 Add test

* 🎉 Add background blur info to plugins API

* 🎉 Suport in wasm for both layer and background blur

* 🐛 Fix failing test

* ♻️ Fix comments

---------

Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-06-16 19:46:03 +02:00
Andrey AntukhandLuis de Dios b391a4c8d3 ♻️ Add mcp integration state management refactor (#10226)
* ♻️ Add mcp integration state management refactor

* 🐛 Fix access tokens do not appear

* ♻️ Refactor some names

* ♻️ Refactor token deletion

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-06-16 18:35:30 +02:00
andrés gonzález 565e173918 📚 Update troubleshooting WebGL doc (#10233) 2026-06-16 16:03:14 +02:00
Eva Marco 7863692c98 🐛 Fix fonts select position (#10192) 2026-06-16 15:28:27 +02:00
Eva Marco 6fed3e1212 🐛 Fix undo on numeric-input drag (#10193) 2026-06-16 15:28:11 +02:00
Andrey Antukh d5e6ea7572 🐛 Fix mcp menu incorrect state when key has not expiration (#10211) 2026-06-16 15:05:49 +02:00
Juanfran 09db565bc2 🐛 Skip org membership lookup for anonymous invite recipients
When an organization invitation token is verified by a logged-out recipient
(e.g. an unregistered invitee opening the emailed link), profile-id is nil.
The team-invitation branch still evaluated get-org-membership eagerly, calling
nitrate with that nil profile-id. That request fails and surfaces as a generic
error, masking the clean :invalid-token response and dropping the user on the
login screen instead of the dedicated "Invite invalid" page.

Only query membership when a logged-in profile is present, so a canceled or
otherwise invalid org invite reaches the :invalid-token path as intended.
2026-06-16 15:01:47 +02:00
Andrey Antukh c55d910e95 Merge remote-tracking branch 'origin/staging' into develop 2026-06-16 13:07:59 +02:00
Andrey Antukh 3dff9b8f28 Merge remote-tracking branch 'origin/main' into staging 2026-06-16 13:07:40 +02:00
Belén Albeza 8b92200488 🐛 Fix double click not editing the guide 2026-06-16 11:21:18 +02:00
Alejandro Alonso 392caa4ec1 Merge pull request #10150 from penpot/elenatorro-remove-to-blob
 Replace toBlob to capture snapshots without blocking the main thread
2026-06-16 10:00:46 +02:00
Alejandro Alonso cd239d24f8 🐛 Fix transparent tile holes in viewer WASM render
Use direct backbuffer tile compositing for render_sync_shape (viewer/thumbnails)
instead of the tile texture atlas cache, which left 512×512 transparent holes.
2026-06-16 09:52:19 +02:00
Alejandro Alonso 92c9517322 Merge pull request #10184 from penpot/azazeln28-fix-frame-stroke
🐛 Board stroke disappears when a layer is placed inside
2026-06-16 09:32:14 +02:00
Belén Albeza d1dd5d9016 🎉 Render guides in wasm (#10014)
*  Remove guides from svg overlay

* 🎉 Draw guides in wasm

* 🎉 Serialize guides to wasm

*  Store separate and sorted horizontal and vertical guides

* 🎉 Implement collision detection with guides

* 🎉 Right click on guides to change color or remove

*  Implement dragging guides

* 🎉 Edit wasm guides by double clicking them

* 🎉 Implement changing mouse cursor on hovering a guide

*  Show guide pill on hover

* 🎉 Implement removing guide on hovering + Del

* 🔧 Fix lint + fmt errors

* 🎉 Clip out outer board guide lines

* ♻️ Extract common code into guide-pill* component

* 🎉 Draw dotted lines on hovering board guides

* 🐛 Fix board rotation when it has guides

* 🎉 Make foreign guides not visible in focus mode
2026-06-16 09:19:58 +02:00
Alejandro Alonso b06942c668 Merge pull request #10191 from penpot/superalex-fix-viewer-webgl-issues
🐛 Fix some viewer webgl issues
2026-06-15 18:54:32 +02:00
Andrey Antukh 8b1845366a 🐛 Allow pasting comma-separated emails in multi-input (#10186)
The multi-input component did not handle paste events for
comma-separated values. When users pasted emails like
'qa@example.com, test@example.com', the entire string was
inserted as-is, triggering validation errors.

The on-key-down handler already split text on commas/spaces
when typing, but paste events bypassed this logic.

Added an on-paste handler that:
- Detects if pasted text contains commas or whitespace
- Splits the text by commas and/or whitespace
- Validates each part individually
- Adds valid items to the items list
- Prevents default paste behavior
- Resets input state after processing

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-15 17:50:59 +02:00
Alejandro Alonso 3802cdf9dd 🐛 Fix overlay canvas misalignment with shadow margin in WASM viewer 2026-06-15 17:16:19 +02:00
Aitor Moreno c54ab3bd34 🐛 Fix frame stroke 2026-06-15 16:09:23 +02:00
David Barragán Merino b03537fa68 ⬆️ Upgrade devenv base imagen to ubuntu 26.04 2026-06-15 14:00:29 +02:00
David Barragán Merino 3f3a52a098 ⬆️ Upgrade imagemagick version and base image to ubuntu 26.04 2026-06-15 14:00:29 +02:00
David Barragán Merino 575bed5c6e 🐛 Add missed info to manage help message 2026-06-15 14:00:29 +02:00
Andrey Antukh 03341ed857 ⬆️ Update npm deps and pnpm on all subpackages (#10183) 2026-06-15 13:57:29 +02:00
Andrey Antukh d44c6250ea Merge remote-tracking branch 'origin/staging' into develop 2026-06-15 13:04:19 +02:00
Marina López 98e04bc5f0 Improve nitrate manual renew banner 2026-06-15 12:30:59 +02:00
Andrey Antukh 61c52a665d ⬆️ Update dependencies (#10166)
* ⬆️ Update exporter dependencies

* ⬆️ Update frontend dependencies

* ⬆️ Update nodejs version devenv and docker images
2026-06-15 12:03:59 +02:00
Andrey Antukh 79227c4de8 Batch multiple thumbnail deletions into a single RPC call (#9943)
*  Batch multiple thumbnail deletions into a single RPC call

Replace the old per-object immediate thumbnail deletion with a
debounced batched approach. The frontend queues object-ids in state
and waits 200ms before sending a single RPC request with up to 200
object-ids. The backend deletes all matching thumbnails in one SQL
statement with a single RETURNING clause, then touches the affected
media objects.

This reduces RPC overhead when rapidly clearing thumbnails (e.g.
navigating pages) and makes deletions more efficient.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 📎 Fix missing issues

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-15 12:01:32 +02:00
Alejandro Alonso a01c4e1bad 🐛 Fix scale viewer WASM canvas by devicePixelRatio like workspace 2026-06-15 11:45:21 +02:00
Juan de la CruzandLuis de Dios ddeaf3ce2a Add MCP status button to workspace toolbar with single-tab connection control (#9930)
*  Add MCP connection badge to the workspace toolbar

*  Add MCP status button with single-tab connection control

* ♻️ Extract component for MCP indicator in the toolbar

* ♻️ Some improvements

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-06-15 11:29:25 +02:00
Alonso Torres 92cf0cda7b 🐛 Fix openPage plugin problem (#10085)
* 🐛 Fix openPage plugin problem

* 🐛 Make history safer for tests
2026-06-15 11:27:35 +02:00
David Barragán Merino 6a1c3348f3 🐳 Clean apt package cache in all Dockerfiles 2026-06-15 11:13:12 +02:00
David Barragán Merino fe86359dc8 🐳 Migrate storybook image to alpine 2026-06-15 11:13:12 +02:00
David Barragán Merino 572b094bab 🐳 Migrate frontend image to alpine 2026-06-15 11:13:12 +02:00
Alejandro Alonso 9d7a8eaeda 🐛 Fix rendering artifacts from previous frames 2026-06-15 11:00:56 +02:00
Luis de Dios 739a2d4958 💄 Update translations for company size options in in-app onboarding (#10164) 2026-06-15 10:57:08 +02:00
Eva Marco c66ee1803f 🎉 Toggle color library visibility from the colorpicker shortcut button (#10129)
* ♻️ Transform a show button to a toggle button on colorpicker

* 🎉 Add test

* 🎉 Add aria-pressed to toggle palette button
2026-06-12 13:30:57 +02:00
María Valderrama 68d4238277 🐛 Fix license not loading in theme change 2026-06-12 12:13:50 +02:00
Andrey Antukh a8d0c18c1b 🐛 Fix race condition between MCP initialization and plugin runtime (#10137)
* 🐛 Fix race condition between MCP init and plugin runtime

Add promise-based synchronization to ensure MCP initialization waits
for plugin runtime to be ready before calling global.ɵloadPlugin.

- Add runtime-ready-promise in app.plugins that resolves when
  init-plugins-runtime completes
- Add wait-for-runtime function for other modules to await readiness
- MCP init now waits for runtime via rx/from before starting plugin
- Add defensive guards in start-plugin!, load-plugin!, close-plugin!
  to check if plugin APIs exist before calling
- Rename init-plugins-runtime! to init-plugins-runtime

Fixes: global.ɵloadPlugin is not a function error when MCP plugin
starts before async plugin runtime initialization completes.

* 📎 Add 'create-pr' opencode skill
2026-06-12 11:40:02 +02:00
Andrey Antukh f5874e159e 🔧 Replace UAParser.js with @penpot/ua-parser (#10007) 2026-06-12 10:46:09 +02:00
Alejandro Alonso 87eb91f805 ♻️ Migrate viewer WASM viewport to modern syntax (#10106) 2026-06-12 10:29:58 +02:00
David Barragán Merino 7cb7f7adb2 🐳 Add command to upgrade packages during the creation 2026-06-12 09:34:26 +02:00
Alonso Torres b2439694af 🐛 Fix float precision in typography line-height/letter-spacing string conversion (#9973) 2026-06-12 09:10:36 +02:00
Alonso Torres d6c973e269 🐛 Improved error handling on a tool error (#10148) 2026-06-12 08:59:05 +02:00
Justin Lin 9c7af3aeb5 📚 Add WebSocket proxy configuration for MCP in Nginx example (#10152)
The current Nginx example doesn't include the required Websocket settings to correctly proxy /mcp/ws, and leads to WebSocket connection error when trying to connect in a design. Adding this lines fixed the issue.

Signed-off-by: Justin Lin <30039756+lancatlin@users.noreply.github.com>
2026-06-12 08:55:23 +02:00
Elena Torro 4a86431dfd Replace toBlob to capture snapshots without blocking the main thread 2026-06-11 17:56:46 +02:00
Andrey Antukh 2e1839f898 🐛 Fix NotReadableError in rasterizer during thumbnail generation
The rasterizer's create-image function was clearing image.src in its
Rx teardown cleanup. This caused the decoded pixel data to be discarded
before downstream operators (drawImage / createImageBitmap) could read
it, resulting in a browser NotReadableError.

Changes:
- Remove image.src = "" from cleanup; the image element will be
  garbage collected naturally. Event handler nulling is kept to break
  circular references.
- Add dimension validation in svg-get-adjusted-size to return nil for
  zero/NaN dimensions instead of producing invalid sizes.
- Add fallback in svg-set-intrinsic-size! to use [max max] when SVG
  dimensions can't be determined.

Error occurred in production (2.16.0-RC10) during thumbnail generation
in the workspace.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-06-11 17:04:24 +02:00
Andrey Antukh 5f21ebd08d 🐛 Filter ignorable React removeChild errors at error boundary and fix HTML anti-pattern (#10145)
* 🐛 Filter ignorable exceptions in error-boundary onError callback

The global uncaught-error-handler already skips NotFoundError/removeChild
and other harmless errors, but react-error-boundary's onError callback fires
independently of the window.onerror pipeline. This means the error boundary
was still logging these errors and setting last-exception, causing them to
continue appearing in error reports despite being non-actionable.

Add the is-ignorable-exception? check to the error-boundary* onError so
harmless errors are silently ignored, matching the behavior of the global
handler.

* 🐛 Fix dangerouslySetInnerHTML anti-pattern in context-notification

The previous code used dangerouslySetInnerHTML on the same element that
could also contain React children. This is a React anti-pattern that can
cause reconciliation mismatches and lead to removeChild DOMExceptions.

Refactor to use two separate element branches: one for raw HTML injection
and one for normal React children with links.
2026-06-11 16:59:00 +02:00
David Barragán Merino 045f177a1f 🐳 Pin docker images to 2.16 2026-06-11 16:48:38 +02:00
David Barragán Merino b2c7854f5e 🐳 Pin docker images to 2.16 2026-06-11 16:48:24 +02:00
David Barragán Merino ab614d2ae9 🐳 Pin docker images to 2.16 2026-06-11 16:47:35 +02:00
David Barragán Merino 11096f1829 🔧 Remove the confirmation step for publishing docker images 2026-06-11 16:26:36 +02:00
David Barragán Merino b1ec03a5d2 🔧 Remove the confirmation step for publishing docker images 2026-06-11 16:26:18 +02:00
Alonso Torres 6cc1c1596f 📚 Improve plugin api docs (#10142) 2026-06-11 15:51:03 +02:00
Andrey Antukh 715bd1c09e 📎 Update mcp server api_types.yml file 2026-06-11 14:51:02 +02:00
Andrey Antukh 6827c4554b 📎 Update version on mcp package.json 2026-06-11 14:49:48 +02:00
Andrey Antukh 8b16326334 📎 Update changelog 2026-06-11 14:49:09 +02:00
Luis de Dios 9670140448 🐛 Fix first element on shared libraries list does not have border (#10062) 2026-06-11 13:35:10 +02:00
Alonso Torres 5b6041624a 🐛 Fix export presets not preserved in view mode inspect (#9972)
* 🐛 Fix export presets not preserved in view mode inspect

* 🐛 Changes after review
2026-06-11 13:33:48 +02:00
Andrey Antukh 2d843da2cd 📎 Update mcp server api_types.yml 2026-06-11 13:31:34 +02:00
Andrey Antukh 70cbb65f9c Merge remote-tracking branch 'origin/staging' into develop 2026-06-11 13:27:43 +02:00
Andrey Antukh 86d508eaf1 Merge remote-tracking branch 'origin/main' into staging 2026-06-11 13:24:57 +02:00
Luis de Dios 331e66c1c6 Update company size options in in-app onboarding (#10110) 2026-06-11 13:23:41 +02:00
Alejandro Alonso 38d12b3363 Add hard reload path for renderer menu A/B test (#10133) 2026-06-11 13:16:09 +02:00
Alonso Torres f5c258b868 🐛 Activate inactive profile when re-registering with disable-email-verification (#9999) 2026-06-11 12:52:07 +02:00
Madalena Melo 777f335a18 📎 Update THANKYOU.md (#10100)
Update THANKYOU.md to include Noob Researcher and Kirubakaran N

Signed-off-by: Madalena Melo <madalena.melo@kaleidos.net>
2026-06-11 12:47:20 +02:00
Andrey Antukh 935fd85e9c 🐛 Fix exporter deps install script 2026-06-11 12:42:43 +02:00
Andrey Antukh 3a386c0ee6 Merge remote-tracking branch 'origin/main' into staging 2026-06-11 11:56:05 +02:00
andrés gonzález a8c1a673a8 📚 Add MCP server block and setup videos to docs (#10121) 2026-06-11 11:46:40 +02:00
David Barragán Merino 726a0cf89f 🐳 Add command to upgrade packages during the creation 2026-06-11 11:13:20 +02:00
Andrey Antukh 5eb8c9ee70 ⬆️ Update frontend deps 2026-06-11 11:06:12 +02:00
Andrey Antukh 12e7ea038f Merge remote-tracking branch 'origin/staging' into develop 2026-06-11 10:58:19 +02:00
Elena Torró 6ebefa2c16 Add single-file export to PDF using the WebGL render (#9860) 2026-06-11 10:41:21 +02:00
Andrey Antukh d65e8317e3 📎 Update changelog 2026-06-11 10:41:05 +02:00
Andrey Antukh 99bf493030 📎 Update the update-changelog skill 2026-06-11 10:40:03 +02:00
Andrey Antukh 3377473f05 Merge remote-tracking branch 'origin/main' into staging 2026-06-11 10:12:12 +02:00
Andrey Antukh ba9b03268a ⬆️ Update backend and common dependencies (#10108)
* 🐛 Fix incorrect valkey uri on backend tests

* ⬆️ Update backend dependencies

* ⬆️ Update pnpm dependencies

* 📎 Fix minor linter issues
2026-06-11 10:11:23 +02:00
Aitor Moreno 17b38e3e6b Merge pull request #10123 from penpot/superalex-fix-wasm-selected-text-hover-double-click
🐛 Keep hover on selected text layers in WASM renderer
2026-06-11 09:47:50 +02:00
María Valderrama 577ddfa03e 🐛 Fix number of days for deletion for nitrate 2026-06-11 09:41:45 +02:00
Belén Albeza 8eedd951a8 🐛 Fix mistmatch in mapping custom font variants (wasm) (#10122) 2026-06-11 09:36:29 +02:00
Alejandro Alonso 435cdcb1d4 🐛 Keep hover on selected text layers in WASM renderer 2026-06-11 09:23:03 +02:00
Andrey Antukh d1fb1d34a1 🐛 Fix email validation to reject consecutive dots in domain (#10096)
* 🐛 Tighten email validation regex to reject consecutive dots in domain

* 📎 Add minor adjustments to gh-issue-from-pr skill
2026-06-10 19:21:51 +02:00
Dexterity 920a05de74 ♻️ Migrate typography sidebar option components to modern syntax (#9443) 2026-06-10 18:16:13 +02:00
Alonso Torres ccd34a2705 🐛 Fix problems with async font loading (#10081) 2026-06-10 14:51:46 +02:00
DexterityandAndrey Antukh 3ffc76a552 ♻️ Migrate token theme-selector components to modern syntax (#9447)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 13:48:11 +02:00
DexterityandAndrey Antukh 0b60e4e16e ♻️ Migrate flex-controls padding/margin/gap to modern syntax (#9458)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 13:47:48 +02:00
DexterityandAndrey Antukh 5d2e0cb7cb ♻️ Migrate viewer/interactions viewport components to modern syntax (#9442)
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 13:46:25 +02:00
Eva Marco 6937e70dab 🎉 Activate by default token typography row (#10090)
* 🎉 Activate by default token typography row

* ♻️ Update test for text multiselection
2026-06-10 13:30:04 +02:00
Andrey Antukh 21d2037111 🔧 Fix default issue templates 2026-06-10 12:36:21 +02:00
Andrey Antukh 5a3458bf74 🔧 Fix typo on github workflows config 2026-06-10 11:36:42 +02:00
Andrey Antukh d04be98644 🔧 Add minor configuration fix for tests-frontend workflow 2026-06-10 11:31:54 +02:00
Andrey Antukh 9f5a0f767e Merge remote-tracking branch 'origin/staging' into develop 2026-06-10 11:26:48 +02:00
Andrey Antukh fa0531cd28 Merge remote-tracking branch 'origin/main' into staging 2026-06-10 11:26:14 +02:00
Andrey Antukh 3419f7e60a ⬆️ Update root repo deps 2026-06-10 11:25:59 +02:00
martrend33 5dea5db685 📎 Add license metadata for MCP package (#10089)
Add the repository license SPDX identifier to the published @penpot/mcp package metadata.

Signed-off-by: martrend33 <145278472+martrend33@users.noreply.github.com>
2026-06-10 11:17:07 +02:00
Andrey Antukh 0ac092d177 🐛 Make set-file-shared idempotent to fix race condition with optimistic updates (#10093) 2026-06-10 10:58:50 +02:00
DexterityandAndrey Antukh a178de2c69 ♻️ Migrate v3 text-editor to modern component syntax (#9444)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 10:57:19 +02:00
DexterityandAndrey Antukh 890ce378a7 ♻️ Migrate template-item to modern component syntax (#9441)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 10:54:46 +02:00
sxxtonyandAndrey Antukh ebb5738c7b ♻️ Migrate dashboard assets to modern syntax (#9404)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 10:52:54 +02:00
sxxtonyandAndrey Antukh bfad78d39c ♻️ Migrate account settings to modern syntax (#9401)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 10:52:14 +02:00
DexterityandAndrey Antukh 32d974aa3e ♻️ Migrate onboarding team-choice to modern component syntax (#9463)
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-06-10 10:51:35 +02:00
Alejandro Alonso becba0a82b 🐛 Fix frame inner strokes ignoring focus mode in wasm renderer 2026-06-10 09:36:28 +02:00
1317 changed files with 137755 additions and 50881 deletions

No files matched your search

+3
View File
@@ -88,6 +88,9 @@
:dynamic-var-not-earmuffed
{:level :off}
:type-mismatch
{:level :off}
:used-underscored-binding
{:level :warning}
+1 -1
View File
@@ -52,7 +52,7 @@ built fresh from `shared/codex.toml` + `templates/codex.toml` at launch.
`manage.sh`.
* **`mcp/`** (Claude Code, opencode) is the result of merging `shared/` with
the port-substituted `templates/`. `manage.sh` writes these on every
`run-devenv-agentic` pass. Gitignored, dedicated paths with no developer
`run-devenv --agentic` pass. Gitignored, dedicated paths with no developer
content — never edit by hand, your edits will be overwritten on the next
reconcile.
* **`.vscode/mcp.json`** is the same merge, but written to the path VS Code
+3
View File
@@ -0,0 +1,3 @@
# Penpot API configuration for error-reports CLI tool
PENPOT_API_URI=http://localhost:3450
PENPOT_ACCESS_TOKEN=your-access-token-here
-1
View File
@@ -1,6 +1,5 @@
description: Create a report to help us improve
name: Bug report
title: ""
type: Bug
labels: ["needs triage"]
@@ -1,7 +1,6 @@
description: Suggest an idea for this project.
labels: ["needs triage"]
name: "Feature request"
title: ""
type: Enhancement
body:
+76
View File
@@ -0,0 +1,76 @@
name: Auto Label and Add to Project
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
jobs:
triage:
runs-on: ubuntu-latest
steps:
- name: Generate GitHub App token
id: triage-app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.TRIAGE_APP_ID }}
private-key: ${{ secrets.TRIAGE_APP_PRIVATE_KEY }}
owner: penpot
- name: Process Issue or PR
uses: actions/github-script@v7
with:
github-token: ${{ steps.triage-app-token.outputs.token }}
script: |
// === 1. CONFIGURATION ===
const PROJECT_NUMBER = 8; // <--- Replace with your project board number
const IS_ORG = true; // <--- Set to false if this is a personal project, true if an organization
const OWNER = context.repo.owner;
const REPO = context.repo.repo;
const issueNumber = context.issue.number;
const isPR = !!context.payload.pull_request;
const contentId = isPR ? context.payload.pull_request.node_id : context.payload.issue.node_id;
// Define your labels here
const labelToApply = 'needs triage';
// === 2. APPLY THE LABEL ===
console.log(`Applying label "${labelToApply}" to ${isPR ? 'PR' : 'Issue'} #${issueNumber}...`);
await github.rest.issues.addLabels({
issue_number: issueNumber,
owner: OWNER,
repo: REPO,
labels: [labelToApply]
});
// === 3. ADD TO PROJECT BOARD ===
console.log(`Fetching Project #${PROJECT_NUMBER} ID...`);
const projectQuery = `
query($owner: String!, $number: Int!) {
${IS_ORG ? 'organization' : 'user'}(login: $owner) {
projectV2(number: $number) {
id
}
}
}
`;
const projectRes = await github.graphql(projectQuery, { owner: OWNER, number: PROJECT_NUMBER });
const projectId = IS_ORG ? projectRes.organization.projectV2.id : projectRes.user.projectV2.id;
console.log(`Adding item to project board...`);
const addToProjectMutation = `
mutation($projectId: ID!, $contentId: ID!) {
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
item {
id
}
}
}
`;
await github.graphql(addToProjectMutation, { projectId, contentId });
console.log("Automation successfully completed!");
+75 -33
View File
@@ -9,16 +9,6 @@ on:
type: string
required: true
default: 'develop'
build_wasm:
description: 'BUILD_WASM. Valid values: yes, no'
type: string
required: false
default: 'yes'
build_storybook:
description: 'BUILD_STORYBOOK. Valid values: yes, no'
type: string
required: false
default: 'yes'
workflow_call:
inputs:
gh_ref:
@@ -26,25 +16,21 @@ on:
type: string
required: true
default: 'develop'
build_wasm:
description: 'BUILD_WASM. Valid values: yes, no'
type: string
required: false
default: 'yes'
build_storybook:
description: 'BUILD_STORYBOOK. Valid values: yes, no'
type: string
required: false
default: 'yes'
concurrency:
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
cancel-in-progress: true
jobs:
build-bundle:
name: Build and Upload Penpot Bundle
# ── 1. Decide whether there is anything to build ───────────────────────
check:
name: Check current bundle
runs-on: penpot-runner-01
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
timeout-minutes: 10
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
bundle_version: ${{ steps.vars.outputs.bundle_version }}
exists: ${{ steps.check.outputs.exists }}
steps:
- name: Checkout repository
@@ -59,10 +45,52 @@ jobs:
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
echo "bundle_version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
# The uploaded zip carries its version as S3 metadata. If the
# existing object was already built from this same commit, the
# whole build job is skipped.
- name: Check if this bundle is already built
id: check
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
EXISTING_VERSION=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "penpot-${{ steps.vars.outputs.gh_ref }}.zip" \
--query 'Metadata."bundle-version"' \
--output text 2>/dev/null || echo "none")
if [ "$EXISTING_VERSION" = "${{ steps.vars.outputs.bundle_version }}" ]; then
echo "exists=true" >> $GITHUB_OUTPUT
{
echo "### ⏭️ Bundle build skipped"
echo ""
echo "The bundle in S3 was already built from \`${{ steps.vars.outputs.bundle_version }}\`."
} >> "$GITHUB_STEP_SUMMARY"
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
# ── 2. Build and upload, only when needed ──────────────────────────────
build:
name: Build and Upload Penpot Bundle
runs-on: penpot-runner-01
timeout-minutes: 90
needs: check
if: needs.check.outputs.exists == 'false'
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
- name: Build bundle
env:
BUILD_WASM: ${{ inputs.build_wasm }}
BUILD_STORYBOOK: ${{ inputs.build_storybook }}
BUILD_WASM: 'yes'
BUILD_STORYBOOK: 'yes'
run: ./manage.sh build-bundle
- name: Prepare directories for zipping
@@ -76,18 +104,32 @@ jobs:
zip -r zips/penpot.zip penpot
- name: Upload Penpot bundle to S3
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip --metadata bundle-version=${{ steps.vars.outputs.bundle_version }}
aws s3 cp zips/penpot.zip \
s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.check.outputs.gh_ref }}.zip \
--metadata bundle-version=${{ needs.check.outputs.bundle_version }}
# ── 3. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
runs-on: penpot-runner-01
timeout-minutes: 5
needs: [check, build]
if: failure()
steps:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 📦 *[PENPOT] Error building penpot bundles.*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
Bundle version: `${{ steps.vars.outputs.bundle_version }}`
📄 Triggered from ref: `${{ needs.check.outputs.gh_ref || inputs.gh_ref }}`
Bundle version: `${{ needs.check.outputs.bundle_version || 'n/a' }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
+6 -2
View File
@@ -11,8 +11,6 @@ jobs:
secrets: inherit
with:
gh_ref: "develop"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
@@ -20,3 +18,9 @@ jobs:
secrets: inherit
with:
gh_ref: "develop"
build-admin-console-docker:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: "develop"
@@ -0,0 +1,91 @@
name: Admin Console Docker Builder
on:
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch or ref to build in penpot-nitrate'
type: string
required: true
default: 'develop'
dispatch_ref:
description: 'Branch of penpot-nitrate from which the workflow definition is read'
type: string
required: false
default: 'develop'
workflow_call:
inputs:
gh_ref:
description: 'Name of the branch or ref to build in penpot-nitrate'
type: string
required: true
dispatch_ref:
description: 'Branch of penpot-nitrate from which the workflow definition is read'
type: string
required: false
default: 'develop'
secrets:
ORG_WORKFLOW_TOKEN:
description: 'Token with Actions write access on penpot-nitrate'
required: true
jobs:
build-nitrate-docker:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.ORG_WORKFLOW_TOKEN }}
REPO: penpot/penpot-nitrate
WORKFLOW: build-docker-admin-console.yml
GH_REF: ${{ inputs.gh_ref }}
DISPATCH_REF: ${{ inputs.dispatch_ref }}
steps:
- name: Trigger nitrate docker build
id: dispatch
run: |
DISTINCT_ID="${{ github.run_id }}-${{ github.run_attempt }}"
CALLER_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$DISPATCH_REF" \
-f gh_ref="$GH_REF" \
-f caller_run_id="$DISTINCT_ID" \
-f caller_run_url="$CALLER_URL"
# Locate the dispatched run using the correlation id embedded in its run-name
RUN_ID=""
for i in $(seq 1 24); do
sleep 5
RUN_ID=$(gh run list --repo "$REPO" --workflow "$WORKFLOW" \
--limit 10 --json databaseId,displayTitle \
--jq ".[] | select(.displayTitle | contains(\"$DISTINCT_ID\")) | .databaseId" \
| head -n1)
[ -n "$RUN_ID" ] && break
done
if [ -z "$RUN_ID" ]; then
echo "::error::Could not locate the dispatched run in $REPO"
exit 1
fi
RUN_URL="https://github.com/$REPO/actions/runs/$RUN_ID"
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
echo "run_url=$RUN_URL" >> "$GITHUB_OUTPUT"
echo "::notice title=Nitrate docker build::$RUN_URL"
- name: Wait for nitrate docker build
run: |
gh run watch "${{ steps.dispatch.outputs.run_id }}" \
--repo "$REPO" \
--interval 30 \
--exit-status
- name: Report result
if: always() && steps.dispatch.outputs.run_id != ''
run: |
CONCLUSION=$(gh run view "${{ steps.dispatch.outputs.run_id }}" \
--repo "$REPO" --json conclusion --jq '.conclusion')
{
echo "### 🐳 Nitrate docker build"
echo ""
echo "- Result: \`${CONCLUSION:-in_progress}\`"
echo "- Run: ${{ steps.dispatch.outputs.run_url }}"
} >> "$GITHUB_STEP_SUMMARY"
+11 -2
View File
@@ -20,12 +20,19 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Registry
- name: Login to Docker Registry (push destination)
uses: docker/login-action@v4
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Login to Docker Hardened Images registry (base image pull)
uses: docker/login-action@v4
with:
registry: dhi.io
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Build and push DevEnv Docker image
uses: docker/build-push-action@v7
env:
@@ -35,12 +42,14 @@ jobs:
file: ./docker/devenv/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
provenance: mode=max
sbom: true
tags: ${{ env.DOCKER_IMAGE }}:latest
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
+214 -111
View File
@@ -16,55 +16,121 @@ on:
required: true
default: 'develop'
concurrency:
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
cancel-in-progress: true
env:
ALL_IMAGES: backend frontend exporter storybook mcp
# All runner instances live on the same server, so the bundle is
# downloaded from S3 once and shared between build jobs through this
# host-local directory. Each build job falls back to S3 if the file is
# missing (e.g. if runners ever move to separate machines).
BUNDLE_CACHE: /var/tmp/penpot-bundle-cache
jobs:
build-and-push:
name: Build and Push Penpot Docker Images
# ── 1. Resolve the build key and check the whole set at once ───────────
prepare:
name: Prepare
runs-on: penpot-runner-02
timeout-minutes: 15
outputs:
gh_ref: ${{ steps.vars.outputs.gh_ref }}
bundle_version: ${{ steps.vars.outputs.bundle_version }}
build_key: ${{ steps.vars.outputs.build_key }}
exists: ${{ steps.check.outputs.exists }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.gh_ref }}
- name: Extract some useful variables
id: vars
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
GH_REF="${{ inputs.gh_ref || github.ref_name }}"
echo "gh_ref=$GH_REF" >> $GITHUB_OUTPUT
BUNDLE_VERSION=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "penpot-$GH_REF.zip" \
--query 'Metadata."bundle-version"' \
--output text)
echo "bundle_version=$BUNDLE_VERSION" >> $GITHUB_OUTPUT
# Image content = bundle + docker build context, so the build key
# combines both.
CTX_HASH=$(git rev-parse "HEAD:docker/images" | cut -c1-12)
echo "build_key=${BUNDLE_VERSION}-${CTX_HASH}" >> $GITHUB_OUTPUT
# The image set is a single block, so a single set-level check is
# enough: `promote` drops a marker object in S3 only after every
# image was built AND every branch tag was moved. Marker present
# means there is nothing at all to do for this build key.
- name: Check if this image set is already built
id: check
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
if aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "markers/images-${{ steps.vars.outputs.build_key }}" \
> /dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
{
echo "### ⏭️ Image set build skipped"
echo ""
echo "The whole set was already built and promoted for \`${{ steps.vars.outputs.build_key }}\`."
} >> "$GITHUB_STEP_SUMMARY"
else
echo "exists=false" >> $GITHUB_OUTPUT
# Stage the bundle in the host-local cache, once, for all the
# build jobs. Download to a temp name and mv for atomicity;
# prune stale bundles while at it.
mkdir -p "$BUNDLE_CACHE"
find "$BUNDLE_CACHE" -type f -mtime +1 -delete || true
ZIP="$BUNDLE_CACHE/penpot-${{ steps.vars.outputs.build_key }}.zip"
if [ ! -f "$ZIP" ]; then
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
mv "$ZIP.$$.tmp" "$ZIP"
fi
fi
# ── 2. One build per image, in parallel, only when needed ──────────────
build:
name: Build ${{ matrix.image }}
runs-on: penpot-runner-02
timeout-minutes: 60
needs: prepare
if: needs.prepare.outputs.exists == 'false'
strategy:
fail-fast: true
# 4 runner slots are available for build jobs on this server; cap the
# matrix at 3 so short jobs (prepare and other workflows' checks)
# never queue behind long builds.
max-parallel: 3
matrix:
image: [backend, frontend, exporter, storybook, mcp]
steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}-${{ matrix.image }}" >> $GITHUB_ENV
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Download Penpot Bundles
id: bundles
env:
FILE_NAME: penpot-${{ steps.vars.outputs.gh_ref }}.zip
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
tmp=$(aws s3api head-object \
--bucket ${{ secrets.S3_BUCKET }} \
--key "$FILE_NAME" \
--query 'Metadata."bundle-version"' \
--output text)
echo "bundle_version=$tmp" >> $GITHUB_OUTPUT
pushd docker/images
aws s3 cp s3://${{ secrets.S3_BUCKET }}/$FILE_NAME .
unzip $FILE_NAME > /dev/null
mv penpot/backend bundle-backend
mv penpot/frontend bundle-frontend
mv penpot/exporter bundle-exporter
mv penpot/storybook bundle-storybook
mv penpot/mcp bundle-mcp
popd
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Registry
uses: docker/login-action@v4
with:
@@ -81,103 +147,140 @@ jobs:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
# Images now build FROM Docker Hardened Images (dhi.io). DHI
# is free (Apache 2.0, no subscription), but pulling from it
# still requires an authenticated login -- a separate `docker
# login` against a different registry host, even though it
# reuses the same PUB_DOCKER_* credentials as the DockerHub
# login above.
- name: Login to Docker Hardened Images registry (base image pull)
uses: docker/login-action@v4
with:
registry: dhi.io
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
# Bundle staged once by `prepare` on this host; the S3 fallback only
# triggers if the cache is unavailable (runners on another machine,
# cache pruned mid-run, ...).
- name: Prepare Penpot bundle
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
ZIP="$BUNDLE_CACHE/penpot-${{ needs.prepare.outputs.build_key }}.zip"
if [ ! -f "$ZIP" ]; then
echo "Bundle not found in host cache; falling back to S3."
mkdir -p "$BUNDLE_CACHE"
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.prepare.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
mv "$ZIP.$$.tmp" "$ZIP"
fi
# Extract only the bundle this job needs.
pushd docker/images
unzip -q "$ZIP" "penpot/${{ matrix.image }}/*"
mv "penpot/${{ matrix.image }}" "bundle-${{ matrix.image }}"
popd
- name: Set up QEMU (stable)
uses: docker/setup-qemu-action@v4
with:
platforms: linux/amd64,linux/arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v6
with:
images:
frontend
backend
exporter
storybook
mcp
images: ${{ matrix.image }}
labels: |
bundle_version=${{ steps.bundles.outputs.bundle_version }}
bundle_version=${{ needs.prepare.outputs.bundle_version }}
- name: Build and push Backend Docker image
- name: Build and push Docker image
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'backend'
BUNDLE_PATH: './bundle-backend'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.backend
file: ./docker/images/Dockerfile.${{ matrix.image }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
provenance: mode=max
sbom: true
# Immutable tag only; branch tags are moved atomically for the
# whole image set by the `promote` job.
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:build-${{ needs.prepare.outputs.build_key }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache,mode=max
- name: Build and push Frontend Docker image
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'frontend'
BUNDLE_PATH: './bundle-frontend'
# ── 3. Move the branch tags of ALL images together ─────────────────────
# Runs only when every build succeeded (default `needs` semantics); if
# the set was already complete, `build` is skipped and so is this job —
# the S3 marker guarantees the branch tags were already moved.
promote:
name: Promote image set
runs-on: penpot-runner-02
timeout-minutes: 10
needs: [prepare, build]
steps:
- name: Set common environment variables
run: |
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Registry
uses: docker/login-action@v4
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.frontend
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Exporter Docker image
uses: docker/build-push-action@v7
- name: Point branch tags to the new build key
run: |
set -e
for image in $ALL_IMAGES; do
docker buildx imagetools create \
-t "${{ secrets.DOCKER_REGISTRY }}/$image:${{ needs.prepare.outputs.gh_ref }}" \
"${{ secrets.DOCKER_REGISTRY }}/$image:build-${{ needs.prepare.outputs.build_key }}"
done
# The marker is written LAST: its presence certifies that all five
# images exist and all branch tags point to this build key.
- name: Write set-completed marker
env:
DOCKER_IMAGE: 'exporter'
BUNDLE_PATH: './bundle-exporter'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.exporter
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
run: |
echo "${{ github.run_id }}" | aws s3 cp - \
"s3://${{ secrets.S3_BUCKET }}/markers/images-${{ needs.prepare.outputs.build_key }}"
{
echo "### ✅ Image set promoted"
echo ""
echo "All \`:${{ needs.prepare.outputs.gh_ref }}\` tags now point to \`build-${{ needs.prepare.outputs.build_key }}\`."
} >> "$GITHUB_STEP_SUMMARY"
- name: Build and push Storybook Docker image
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'storybook'
BUNDLE_PATH: './bundle-storybook'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.storybook
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push MCP Docker image
uses: docker/build-push-action@v7
env:
DOCKER_IMAGE: 'mcp'
BUNDLE_PATH: './bundle-mcp'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.mcp
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
# ── 4. Single failure notification for the whole workflow ─────────────
notify:
name: Notify failure
runs-on: penpot-runner-02
timeout-minutes: 5
needs: [prepare, build, promote]
if: failure()
steps:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🐳 *[PENPOT] Error building penpot docker images.*
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
📦 Bundle: `${{ steps.bundles.outputs.bundle_version }}`
❌ 🐳 *[PENPOT] Error building/promoting the penpot docker image set.*
📄 Triggered from ref: `${{ needs.prepare.outputs.gh_ref || inputs.gh_ref }}`
📦 Bundle: `${{ needs.prepare.outputs.bundle_version || 'n/a' }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
+6 -2
View File
@@ -11,8 +11,6 @@ jobs:
secrets: inherit
with:
gh_ref: "staging"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
@@ -20,3 +18,9 @@ jobs:
secrets: inherit
with:
gh_ref: "staging"
build-admin-console-docker:
uses: ./.github/workflows/build-docker-admin-console.yml
secrets: inherit
with:
gh_ref: "staging"
+1 -4
View File
@@ -12,8 +12,6 @@ jobs:
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
build_wasm: "yes"
build_storybook: "yes"
build-docker:
needs: build-bundle
@@ -26,10 +24,9 @@ jobs:
name: Notifications
runs-on: ubuntu-24.04
needs: build-docker
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
+1 -1
View File
@@ -131,7 +131,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
+1 -1
View File
@@ -114,7 +114,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
@@ -129,7 +129,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
+1 -1
View File
@@ -103,7 +103,7 @@ jobs:
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
+7 -6
View File
@@ -62,14 +62,15 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v6
- name: Fmt
working-directory: ./backend
run: |
cljfmt check --parallel=true src/ test/
- name: Lint
working-directory: ./backend
run: |
corepack enable;
corepack install;
pnpm install;
pnpm run check-fmt
pnpm run lint
clj-kondo --parallel --lint ../common/src/ src/
- name: Tests
working-directory: ./backend
@@ -81,4 +82,4 @@ jobs:
run: |
mkdir -p /tmp/penpot;
clojure -M:dev:test --reporter kaocha.report/documentation
clojure -M:dev:test
@@ -0,0 +1,69 @@
name: "CI: Composable Test Suite"
# Runs the composable component test suite (it exercises component semantics
# through the real Plugin API against the full frontend, so it needs the
# frontend bundle + the plugin runtime, but no backend): the driver serves the
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
# fixtures. See plugins/apps/composable-test-suite/README.md ("Running in CI").
defaults:
run:
shell: bash
on:
pull_request:
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
composable-test-suite:
if: ${{ !github.event.pull_request.draft }}
name: "Run composable test suite (mocked backend)"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- uses: actions/checkout@v6
# The driver serves the prebuilt bundle from frontend/resources/public.
- name: Build frontend bundle
working-directory: ./frontend
run: ./scripts/build
- name: Install deps
working-directory: ./plugins
run: |
corepack enable;
corepack install;
pnpm install;
- name: Install Playwright Chromium
working-directory: ./plugins
run: pnpm --filter composable-test-suite exec playwright install --with-deps chromium
- name: Run composable test suite (mocked)
working-directory: ./plugins
run: pnpm --filter composable-test-suite run test:ci
+4 -4
View File
@@ -7,9 +7,9 @@ defaults:
on:
pull_request:
paths:
- 'frotend/**/*'
- 'common/**/*'
- 'render-wasm/**/*'
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
types:
- opened
@@ -22,7 +22,7 @@ on:
- staging
paths:
- 'frotend/**'
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
+2 -2
View File
@@ -7,7 +7,7 @@ defaults:
on:
pull_request:
paths:
- 'frotend/**'
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
@@ -22,7 +22,7 @@ on:
- staging
paths:
- 'frotend/**'
- 'frontend/**'
- 'common/**'
- 'render-wasm/**'
+5
View File
@@ -45,3 +45,8 @@ jobs:
pnpm run fmt:check;
pnpm -r run build;
pnpm -r run types:check;
- name: Tests
working-directory: ./mcp
run: |
pnpm run test;
@@ -0,0 +1,133 @@
name: "CI: Plugin API Test Suite"
# Runs the Plugin API Test Suite (it exercises the real Penpot Plugin API, so it
# needs a running frontend + the plugin runtime). Two jobs:
#
# - api-test-suite-mocked (pull_request / push): the per-PR gate. Serves the
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
# (MOCK_BACKEND=1). No backend / no login. Validates the frontend Plugin API
# binding + in-memory store; backend-result-dependent tests are skipped via the
# `skipIfMocked` tag. See plugins/apps/plugin-api-test-suite/README.md.
#
# - api-test-suite-live (workflow_dispatch): true end-to-end against a LIVE
# instance. Point PENPOT_BASE_URL at a reachable instance and provide login
# credentials via repo secrets. Manual because the CI runner has no Docker to
# stand up a full stack.
defaults:
run:
shell: bash
on:
workflow_dispatch:
inputs:
base_url:
description: "Penpot base URL (e.g. https://localhost:3449)"
required: false
default: "https://localhost:3449"
pull_request:
paths:
- 'plugins/**'
- 'frontend/**'
- 'common/**'
types:
- opened
- synchronize
- ready_for_review
push:
branches:
- develop
- staging
paths:
- 'plugins/**'
- 'frontend/src/app/plugins/**'
- 'common/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
api-test-suite-mocked:
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
name: "Run Plugin API Test Suite (mocked)"
runs-on: penpot-runner-02
container:
image: penpotapp/devenv:latest
volumes:
- /var/cache/github-runner/m2:/root/.m2
- /var/cache/github-runner/gitlib:/root/.gitlibs
steps:
- uses: actions/checkout@v6
# Mocked mode serves the prebuilt bundle from frontend/resources/public.
- name: Build frontend bundle
working-directory: ./frontend
run: ./scripts/build
- name: Install deps
working-directory: ./plugins
run: |
corepack enable;
corepack install;
pnpm install;
- name: Install Playwright Chromium
working-directory: ./plugins
run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
- name: Generate API surface
working-directory: ./plugins
run: pnpm --filter plugin-api-test-suite run gen:api
- name: Run API test suite (mocked)
working-directory: ./plugins
env:
MOCK_BACKEND: "1"
run: pnpm --filter plugin-api-test-suite run test:ci
## The following job will launch the whole suite of tests but we need
## to have a full environment in the CI for this to work.
# api-test-suite-live:
# if: ${{ github.event_name == 'workflow_dispatch' }}
# name: Run Plugin API Test Suite (live)
# runs-on: penpot-runner-02
# container:
# image: penpotapp/devenv:latest
#
# env:
# PENPOT_BASE_URL: ${{ github.event.inputs.base_url }}
# E2E_LOGIN_EMAIL: ${{ secrets.E2E_LOGIN_EMAIL }}
# E2E_LOGIN_PASSWORD: ${{ secrets.E2E_LOGIN_PASSWORD }}
#
# steps:
# - uses: actions/checkout@v6
#
# - name: Setup Node
# uses: actions/setup-node@v6
# with:
# node-version-file: .nvmrc
#
# - name: Install deps
# working-directory: ./plugins
# run: |
# corepack enable;
# corepack install;
# pnpm install;
#
# - name: Install Playwright Chromium
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
#
# - name: Generate API surface
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite run gen:api
#
# # Note: requires a running Penpot instance reachable at PENPOT_BASE_URL.
# - name: Run API test suite
# working-directory: ./plugins
# run: pnpm --filter plugin-api-test-suite run test:ci
+1 -7
View File
@@ -40,19 +40,13 @@ jobs:
steps:
- uses: actions/checkout@v6
- name: Setup Node
id: setup-node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
pnpm install;
pnpm install -r;
- name: Run Lint
working-directory: ./plugins
+8 -3
View File
@@ -9,10 +9,13 @@
.clj-kondo
.cpcache
.lsp
.env
.nrepl-port
.nyc_output
.rebel_readline_history
.repl
opencode.json
.opencode/package-lock.json
/*.jpg
/*.md
!CHANGES.md
@@ -30,7 +33,6 @@
/.clj-kondo/.cache
/_dump
/notes
/.opencode/package-lock.json
/plans
/prompts
/playground/
@@ -72,8 +74,6 @@
/frontend/target/
/frontend/test-results/
/frontend/.shadow-cljs
/other/
/scripts/
/nexus/
/tmp/
/vendor/**/target
@@ -88,6 +88,7 @@
/blob-report/
/playwright/.cache/
/render-wasm/target/
/media-processor/dist/
/**/node_modules
/**/.yarn/*
/.pnpm-store
@@ -98,5 +99,9 @@
/.playwright-mcp
/.devenv/mcp/
/opencode.json
/.opencode/plans
/.opencode/reports
/.opencode/prompts
/.ci-logs
/.codex/
/tools/__pycache__
+1 -1
View File
@@ -1 +1 @@
v22.22.0
v24.18.1
-28
View File
@@ -1,28 +0,0 @@
---
name: commiter
description: Git commit assistant
mode: all
---
## Role
You are responsible for creating git commits for Penpot and must
follow the repository commit-format rules exactly. It should have
concise title and clear summary of changes in the description,
including the rationale if proceed.
## Requirements
* Override your internal commit rules when the user explicitly requests
something that conflicts with them.
* Read `.serena/memories/workflows/creating-commits.md` before
creating any commit and follow the commit guidelines strictly.
* Keep the description (commit body) with maximum line length of 80
characters. Use manual line breaks to wrap text before it exceeds
this limit.
* Use `git commit -s` so the commit includes the required
`Signed-off-by` line.
* Do not guess or hallucinate git author information (Name or
Email). Never include the `--author` flag in git commands unless
specifically instructed by the user for a unique case; assume the
local environment is already configured.
-25
View File
@@ -1,25 +0,0 @@
---
name: Engineer
description: Senior Full-Stack Software Engineer
mode: primary
---
## Role
You are a high-autonomy Senior Full-Stack Software Engineer working on Penpot, an
open-source design tool. You have full permission to navigate the codebase, modify files,
and execute commands to fulfill your tasks. Your goal is to solve complex technical tasks
with high precision while maintaining a strong focus on maintainability and performance.
## Before Start
**Read `AGENTS.md` file and project structure and how the memory system works**
## Requiremens
* Before writing code, analyze the task in depth and describe your plan. If the task is
complex, break it down into atomic steps.
* Do **not** touch unrelated modules unless the task explicitly requires it.
* Only reference functions, namespaces, or APIs that actually exist in the
codebase. Verify their existence before citing them. If unsure, search first.
* Be concise and autonomous — avoid unnecessary explanations.
-60
View File
@@ -1,60 +0,0 @@
---
name: Planner
description: Software architect for planning and analysis only
mode: primary
permission:
edit: ask
---
## Role
You are a Senior Software Architect working on Penpot, an open-source design
tool. Your sole responsibility is planning and analysis — you do NOT write,
modify any code.
You help users understand the codebase, design solutions, and create detailed
implementation plans that other agents or developers can execute. Document
everything they need to know: which files to touch for each task, code, testing,
docs they might need to check, how to test it. Give them the whole plan as
bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
Do **not** suggest commit messages or commit names anywhere in your plans or
responses — committing is the developer's responsibility.
Assume they are a skilled developer, but know almost nothing about our toolset
or problem domain. Assume they don't know good test design very well.
## Requirements
* Analyze the codebase architecture and identify affected modules.
* Read `AGENTS.md` file and project structure and how the memory system works and how to
navigate and read relevant information conventions.
* Break down complex features or bugs into atomic, actionable steps.
* Propose solutions with clear rationale, trade-offs, and sequencing.
* Identify risks, edge cases, and testing considerations.
Save plans to: plans/YYYY-MM-DD-<plan-one-line-title>.md
## Constraints
* You are **read-only** — never create, edit, or delete files.
* You do **not** run builds, tests, linters, or any commands that modify state.
* You do **not** create git commits or interact with version control.
* You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
`find`, `cat`).
* Your output is a structured plan or analysis, ready for handoff to an
engineer agent or developer.
## Output format
When producing a plan, structure it as:
1. **Context** — What is the problem or feature request?
2. **Affected modules** — Which parts of the codebase are involved?
3. **Approach** — Step-by-step implementation plan with file paths and
function names where applicable.
4. **Risks & considerations** — Edge cases, performance implications, breaking
changes.
5. **Testing strategy** — How to verify the implementation works correctly.
-56
View File
@@ -1,56 +0,0 @@
---
name: Prompt Assistant
description: Refines and improves prompts for maximum clarity and effectiveness
mode: all
---
## Role
You are an expert Prompt Engineer with strong knowledge of
penpot. Your sole responsibility is to take a prompt provided by the
user and transform it into the most effective, clear, and
well-structured version possible — ready to be used with any AI model.
## Requirements
* You do NOT execute tasks. You do NOT write code. You only design and refine prompts
* Read `AGENTS.md` file and project structure and how the memory system works and how to
navigate and read relevant information conventions.
* Analyze the original prompt: identify its intent, target audience, ambiguities, missing
context, and structural weaknesses
* Ask clarifying questions if the intent is unclear or if critical information is missing
(e.g. target model, expected output format, tone, constraints). Keep questions concise
and grouped
* Rewrite the prompt using prompt engineering best practices
## Prompt Engineering Principles
Apply these techniques when refining prompts:
- **Be specific and explicit**: Replace vague instructions with precise ones.
- **Set the context**: Include background information the model needs to
perform well.
- **Specify the output format**: State the desired structure, length, tone,
or format (e.g. bullet list, JSON, step-by-step).
- **Add constraints**: Include what the model should avoid or not do.
- **Use examples** (few-shot): When applicable, suggest adding examples to
anchor the model's behaviour.
- **Break down complexity**: Split multi-step tasks into clear numbered steps.
- **Avoid ambiguity**: Remove pronouns and references that could be
misinterpreted.
- **Chain of thought**: For reasoning tasks, include "Think step by step."
## Constraints
- Do NOT execute the prompt yourself.
- Do NOT answer the question inside the prompt.
- Do NOT add unnecessary verbosity — prompts should be as short as they can
be while remaining complete.
- Always preserve the user's original intent.
## Output
Refined Prompt: The improved, ready-to-use prompt. Print it for
immediate use and save it to
prompts/YYYY-MM-DD-N-<prompt-one-line-title>.md for future use.
+42
View File
@@ -0,0 +1,42 @@
---
description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the create-commit skill
agent: build
---
# Implement Plan
This command is run once a plan is ready (for example, from plan mode). Execute
the plan already prepared in the current session context — it does not take
extra arguments. Follow these steps in order.
## 1. Create the issue
Use the **`create-issue`** skill, following the *Creating Issues from Draft Body*
flow in `mem:workflow/creating-issues`. Derive the issue title and body from the
plan. Capture the new issue's number — call it **NNNN** (needed for the branch
name and the commit reference).
## 2. Create the branch
Create and switch to a branch named after the issue:
```
git checkout -b issue-NNNN
```
(Replace NNNN with the issue number from step 1.)
## 3. Execute the plan
Implement the prepared plan from the session context. Work methodically, keeping
changes focused on what the issue requires. Do not commit — the commit happens in
step 4.
## 4. Commit with the create-commit skill
After the implementation is complete, load the **`create-commit`** skill and
follow its workflow to commit the changes. Provide a brief summary of what was
implemented and why, the issue reference (`issue-NNNN`), and the model name you
are running as so the `AI-assisted-by` trailer is set correctly.
Do not push. Pushing is handled separately by the user.
+79
View File
@@ -0,0 +1,79 @@
Act as a senior software engineer and perform a thorough code review.
## Instructions
1. Load the **`code-review-and-quality`** skill — it defines the five axes, core principles (DRY, KISS, YAGNI), severity taxonomy, and output format.
2. Determine the diff or code to review from the provided context.
3. **Skip generated files, lockfile-only changes, and unrelated modifications** unless they introduce security risks.
4. Read the diff and the surrounding context for each changed file.
5. Review across all five axes: correctness, readability, architecture, security, performance.
6. Produce the review using this structure:
- **Summary**: One-paragraph overview of the change and its impact
- **Critical/High Findings**: Blockers that must be fixed (with file:line, severity, description, and proposed fix)
- **Other Findings**: Medium/Low issues and suggestions
- **Testing Recommendations**: Missing test coverage or test quality issues
- **Positive Observations**: What was done well (brief, specific)
- **Verdict**: Approve / Request Changes / Needs Discussion
7. For each finding:
- State the severity (Critical / High / Medium / Low / Suggestion)
- Identify the file and line
- Describe failure circumstances
- **For Critical/High**: Provide a concrete fix with a code snippet showing the corrected code
- **For Medium/Low**: Describe the fix clearly; code snippet optional
- If multiple approaches exist, briefly note trade-offs
8. **Perform a second review pass if the change is complex:**
- **Complex indicators**: Critical/High findings, multiple files (>5), architectural changes, security-sensitive code, >300 lines changed
- **Skip for simple changes**: Typo fixes, formatting, small bug fixes (<50 lines), single-file changes with no findings
- Second pass checks:
- Validate severity assignments: Are Critical/High findings truly blockers?
- Catch missed issues: Edge cases, error paths, test gaps overlooked in first pass
- Remove false positives: Discard findings that aren't real issues
- Verify fixes: Are the proposed solutions actually correct and complete?
## Strong Rules
1. Do not invent problems. Every finding must be real and actionable.
2. Do not modify any code and do not create a commit — this command only reviews.
3. Be specific and constructive. "This could be better" is not helpful — explain why and how.
4. Prioritize by impact. One structural issue outweighs ten nits.
5. If tests are missing for new functionality, flag it as High severity.
## Context
$ARGUMENTS
## Expected Format
```
## Review Summary
[1-2 sentences on what the change does and overall assessment]
## Critical/High Findings
### [Severity] file.ts:123
**Issue**: [Description of the problem]
**Impact**: [What could go wrong]
**Fix**:
```[language]
// Current code
[problematic code]
// Fixed code
[corrected code]
```
[Optional: note trade-offs if multiple approaches exist]
## Other Findings
### [Severity] file.ts:456
**Issue**: [Description]
**Fix**: [Clear description; code snippet optional]
## Testing Recommendations
[List specific test cases that should be added]
## Positive Observations
[2-3 specific things done well]
## Verdict
[Approve / Request Changes / Needs Discussion]
[If Request Changes: list the must-fix items]
```
+299
View File
@@ -0,0 +1,299 @@
import { tool } from "@opencode-ai/plugin"
import path from "path"
import { spawn } from "child_process"
const penpotPsqlTool = tool({
description:
"Execute a SQL command against the Penpot database. Uses the defaults from scripts/psql.",
args: {
sql: tool.schema
.string()
.describe("SQL command to execute"),
test: tool.schema
.boolean()
.describe("Use the penpot_test database")
.optional(),
},
async execute(args, context) {
const host = process.env.PENPOT_DB_HOST || "postgres"
const user = process.env.PENPOT_DB_USER || "penpot"
const db = args.test
? "penpot_test"
: process.env.PENPOT_DB_NAME || "penpot"
const password = process.env.PENPOT_DB_PASSWORD || "penpot"
const psqlArgs = ["-h", host, "-U", user, "-d", db, "-c", args.sql]
return new Promise((resolve) => {
let stdout = ""
let stderr = ""
const proc = spawn("psql", psqlArgs, {
cwd: context.worktree,
env: { ...process.env, PGPASSWORD: password },
})
proc.stdout.on("data", (data) => {
stdout += data.toString()
})
proc.stderr.on("data", (data) => {
stderr += data.toString()
})
proc.on("error", (error) => {
resolve(`Error: ${error.message}`)
})
proc.on("close", (exitCode) => {
const output =
exitCode === 0
? stdout.trim() || "Query executed successfully"
: `Error (exit ${exitCode}): ${
(stderr || stdout).trim() || "No error output"
}`
resolve(output)
})
})
},
})
const parenRepairTool = tool({
description:
"Fix mismatched parentheses/braces in Clojure files (.clj, .cljs, .cljc) then reformat with cljfmt.",
args: {
// A string is used instead of an array so OpenCode displays it
// in the generic tool invocation.
files: tool.schema
.string()
.describe(
"Comma-separated file paths to fix, for example: frontend/src/app/config.cljs, backend/src/core.clj",
)
.optional(),
code: tool.schema
.string()
.describe("Code string to fix via stdin")
.optional(),
},
async execute(args, context) {
const script = path.join(context.worktree, "scripts/paren-repair")
const files = args.files
? args.files
.split(",")
.map((file) => file.trim())
.filter(Boolean)
: []
const paramInfo =
files.length > 0
? `files=[${files.join(", ")}]`
: args.code !== undefined
? `code=(${args.code.length} chars)`
: "none"
return new Promise((resolve) => {
const childArgs =
files.length > 0
? [script, ...files]
: [script]
const proc = spawn("bb", childArgs, {
cwd: context.worktree,
})
let stdout = ""
let stderr = ""
proc.stdout.on("data", (data) => {
stdout += data.toString()
})
proc.stderr.on("data", (data) => {
stderr += data.toString()
})
proc.on("error", (error) => {
resolve(`Error: ${error.message}`)
})
proc.on("close", (exitCode) => {
const output =
exitCode === 0
? stdout.trim() || "No changes needed"
: `Error (exit ${exitCode}): ${
(stderr || stdout).trim() || "No error output"
}`
resolve(output)
})
// Close stdin in all cases so the process cannot wait indefinitely.
if (args.code !== undefined) {
proc.stdin.end(args.code)
} else {
proc.stdin.end()
}
})
},
})
export default async function plugin() {
return {
tool: {
"paren-repair": parenRepairTool,
"penpot-psql": penpotPsqlTool,
},
}
}
// import { tool } from "@opencode-ai/plugin"
// import path from "path"
// import { spawn } from "child_process"
// function formatFiles(files) {
// if (files.length === 0) return "stdin"
// // Keep the visible tool title reasonably short.
// if (files.length <= 3) return files.join(", ")
// return `${files.slice(0, 3).join(", ")} (+${files.length - 3} more)`
// }
// const parenRepairTool = tool({
// description:
// "Fix mismatched parentheses/braces in Clojure files, then reformat with cljfmt.",
// args: {
// files: tool.schema
// .array(tool.schema.string())
// .describe("Array of file paths to fix")
// .optional(),
// code: tool.schema
// .string()
// .describe("Code string to fix via stdin")
// .optional(),
// },
// async execute(args, context) {
// const script = path.join(context.worktree, "scripts/paren-repair")
// const files = (args.files ?? []).map((file) => {
// const absolute = path.isAbsolute(file)
// ? file
// : path.resolve(context.worktree, file)
// return path.relative(context.worktree, absolute)
// })
// const targetSummary =
// files.length > 0
// ? formatFiles(files)
// : args.code !== undefined
// ? `stdin (${args.code.length} chars)`
// : "no input"
// // This updates the tool-call title immediately, while it is running.
// await context.metadata({
// title: `Paren repair: ${targetSummary}`,
// metadata: {
// files,
// codeChars: args.code?.length,
// },
// })
// const childArgs =
// args.files && args.files.length > 0
// ? [script, ...args.files]
// : [script]
// return new Promise((resolve) => {
// const proc = spawn("bb", childArgs, {
// cwd: context.worktree,
// })
// let stdout = ""
// let stderr = ""
// if (args.code !== undefined) {
// proc.stdin.end(args.code)
// }
// proc.stdout.on("data", (data) => {
// stdout += data.toString()
// })
// proc.stderr.on("data", (data) => {
// stderr += data.toString()
// })
// proc.on("close", (exitCode) => {
// const successful = exitCode === 0
// const commandOutput = successful
// ? stdout.trim() || "No changes needed"
// : `Error (exit ${exitCode}): ${(stderr || stdout).trim()}`
// const parameterOutput =
// files.length > 0
// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}`
// : args.code !== undefined
// ? `Input passed through stdin: ${args.code.length} characters`
// : "No files or stdin input were passed"
// resolve({
// title: `Paren repair: ${targetSummary}`,
// output: `${parameterOutput}\n\n${commandOutput}`,
// metadata: {
// files,
// codeChars: args.code?.length,
// exitCode,
// successful,
// },
// })
// })
// proc.on("error", (error) => {
// resolve({
// title: `Paren repair failed: ${targetSummary}`,
// output: [
// files.length > 0
// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}`
// : `Input: ${targetSummary}`,
// `Failed to start bb: ${error.message}`,
// ].join("\n\n"),
// metadata: {
// files,
// codeChars: args.code?.length,
// successful: false,
// },
// })
// })
// })
// },
// })
// export default async function plugin() {
// return {
// tool: {
// "paren-repair": parenRepairTool,
// },
// }
// }
-85
View File
@@ -1,85 +0,0 @@
---
name: backport-commit
description: Port changes from a specific Git commit to the current branch by manually applying the diff, avoiding cherry-pick when it would introduce complex conflicts.
---
# Backport Commit
Port changes from a specific Git commit to the current branch by manually
applying the diff, avoiding `git cherry-pick` when it would introduce
complex conflicts.
## When to Use
Use this skill whenever the user asks to backport a commit, especially when:
- The commit touches multiple modules or files with significant divergence
- `git cherry-pick` is explicitly ruled out ("do not use cherry-pick")
- The target commit is old enough that conflicts are likely
- The commit introduces both source changes AND new files (tests, etc.)
- You need full control over how each hunk is applied
## Workflow
### 1. Identify the target commit
```bash
# Verify the commit exists and understand what it does
git log --oneline -1 <commit-sha>
# Get the full diff (including new/deleted files)
git show <commit-sha>
# Capture the original commit message for later reuse
git log --format='%B' -1 <commit-sha>
```
### 2. Identify affected modules
From the file paths in the diff, determine which Penpot modules are affected
(frontend, backend, common, render-wasm, etc.) and read their `AGENTS.md`
files **before** making any changes. If a module has no `AGENTS.md`, skip
that step — verify with `ls <module>/AGENTS.md` first.
### 3. Read the current state of each affected file
For every file the diff touches, read the current version on disk to understand
context and ensure correct placement before editing.
### 4. Apply changes manually (the core of this approach)
Process every hunk in the diff using the appropriate tool:
| Diff action | Tool to use |
|-------------|-------------|
| Modify existing file | `edit` — use enough surrounding context in `oldString` to uniquely match the location |
| Add new file | `write` — include proper license header and namespace conventions matching project style |
| Delete file | `bash rm <path>` |
| Rename/move file | `bash mv <old> <new>`, then apply any content changes with `edit` |
> **Tip:** Group nearby hunks from the same file into a single `edit` call.
> Use separate calls when hunks are far apart to keep `oldString` short and
> unambiguous.
Repeat until **all** hunks in the diff are ported.
### 5. Validate
Run **lint**, **check-fmt**, and **tests** for every affected module (see each
module's `AGENTS.md` for the exact commands). If the formatter auto-fixes
indentation, verify the logic is still semantically correct. All checks must
pass before moving on.
### 6. Commit
Ask the `commiter` sub-agent to create a commit. Stage all relevant files
(exclude unrelated untracked files) and provide the original commit message as
a reference, adapting it as needed for the target branch context.
## Key Principles
- **Context matters** — always read files before editing; never guess
indentation or surrounding code
- **Lint + format + test** — never skip validation before committing
- **Preserve intent** — keep the original commit message meaning; the
`commiter` agent handles formatting
@@ -0,0 +1,255 @@
---
name: code-review-and-quality
description: Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.
---
# Code Review and Quality
## Overview
Multi-dimensional code review with quality gates. Every change gets reviewed before merge — no exceptions. Review covers five axes: correctness, readability, architecture, security, and performance.
**The approval standard:** Approve a change when it definitely improves overall code health, even if it isn't perfect. Perfect code doesn't exist — the goal is continuous improvement. Don't block a change because it isn't exactly how you would have written it. If it improves the codebase and follows the project's conventions, approve it.
## When to Use
- Before merging any PR or change
- After completing a feature implementation
- When another agent or model produced code you need to evaluate
- When refactoring existing code
- After any bug fix (review both the fix and the regression test)
## Core Principles
These principles underpin every axis. When in doubt, default to them.
- **DRY (Don't Repeat Yourself):** Every piece of knowledge has one authoritative representation. If the same logic appears in two places, extract it into a shared helper, model, or type. Reviewers: flag duplicated logic as a required change — it's not "just similar," it's drift that will diverge.
- **KISS (Keep It Simple, Stupid):** The simplest solution that works is the best solution. Complexity must earn its place. Reviewers: if you need more than one sentence to explain what a piece of code does, it's too complex — push for simplification before merge.
- **YAGNI (You Aren't Gonna Need It):** Don't add abstractions, hooks, or generalizations for hypothetical future use cases. Generalize on the third occurrence, not the first. Reviewers: delete speculative generality.
- **Don't invent problems:** Do not manufacture issues to produce more feedback. Every finding must be a real risk, a real readability barrier, or a real architectural concern — not a hypothetical or a stylistic preference disguised as a problem.
## The Five-Axis Review
Every review evaluates code across these dimensions.
### 1. Correctness
Does the code do what it claims to do?
- Does it match the spec or task requirements?
- Are edge cases handled (null, empty, boundary values)?
- Are error paths handled (not just the happy path)?
- Does it pass all tests? Are the tests actually testing the right things?
- Are there off-by-one errors, race conditions, or state inconsistencies?
### 2. Readability & Simplicity
Can another engineer (or agent) understand this code without the author explaining it?
- Are names descriptive and consistent with project conventions? (No `temp`, `data`, `result` without context)
- Is the control flow straightforward (avoid nested ternaries, deep callbacks)?
- Are there any "clever" tricks that should be simplified?
- **KISS check:** Is this the simplest approach that solves the problem? A 20-line straightforward function beats a 5-line clever one that requires a comment to explain.
- Could this be done in fewer lines? (1000 lines where 100 suffice is a failure)
- Are abstractions earning their complexity? (Don't generalize until the third use case)
- Is a new conditional bolted onto an unrelated flow? Push the logic into its own helper, state, or policy.
- Do repeated conditionals on the same shape appear? They signal a missing model or dispatcher.
- Are there dead code artifacts: no-op variables, backwards-compat shims, or `// removed` comments?
### 3. Architecture
Does the change fit the system's design?
- Does it follow existing patterns or introduce a new one? If new, is it justified?
- Does it maintain clean module boundaries?
- **DRY check:** Is there existing code that does the same thing? Reuse the canonical helper instead of writing a near-duplicate. If two branches do nearly the same thing, collapse them.
- Are dependencies flowing in the right direction (no circular dependencies)?
- Is the abstraction level appropriate (not over-engineered, not too coupled)?
- Does this refactor reduce complexity or just relocate it? Count the concepts a reader must hold. Prefer the restructuring that makes whole branches disappear over one that re-centralizes the same logic. Prefer deleting an abstraction to polishing it.
- Is feature-specific logic leaking into a shared or general-purpose module?
- Are type boundaries explicit? Question gratuitous `any`/`unknown`/optional/casts and silent fallbacks.
- **Structural remedies:** When you flag a problem, propose the move — not just the problem. Replace conditionals with dispatchers, collapse duplicate branches, separate orchestration from business logic, extract helpers, split large files. Prefer the remedy that removes moving pieces over one that spreads the same complexity around.
### 4. Security
For detailed security guidance, see `security-and-hardening`.
- Is user input validated and sanitized?
- Are secrets kept out of code, logs, and version control?
- Is authentication/authorization checked where needed?
- Are SQL queries parameterized (no string concatenation)?
- Are outputs encoded to prevent XSS?
- Are dependencies from trusted sources with no known vulnerabilities?
- Is data from external sources (APIs, logs, user content, config files) treated as untrusted?
### 5. Performance
- Any N+1 query patterns?
- Any unbounded loops or unconstrained data fetching?
- Any synchronous operations that should be async?
- Any unnecessary re-renders in UI components?
- Any missing pagination on list endpoints?
- Any large objects created in hot paths?
## Review Process
1. **Understand the intent** — What is this change trying to accomplish? What spec or task does it implement?
2. **Review tests first** — Tests reveal intent and coverage. Do they test behavior, not implementation details? Are edge cases covered?
3. **Review the implementation** — Walk through each file with the five axes in mind.
4. **Categorize findings** — Label every comment with its severity:
| Prefix | Meaning | Author Action |
|--------|---------|---------------|
| **Critical:** | Blocks merge | Security vulnerability, data loss, broken functionality |
| **High:** | Required change | Must address before merge |
| **Medium:** | Should fix | Strongly recommended, not a blocker |
| **Low:** | Minor, optional | Author may ignore — formatting, style preferences |
| **Suggestion:** | Worth considering | Not required, but improves the code |
For each finding, describe the circumstances under which it could fail: specific inputs, load conditions, timing, or user actions that trigger the problem. "This crashes when input is null" is actionable; "this might crash" is not.
Lead with what matters: correctness and security first, then structural issues, then everything else. A few high-conviction comments beat a long list.
5. **Verify the verification** — What tests were run? Did the build pass? Was the change tested manually? Screenshots for UI changes?
## Review Output
Structure every review using this format:
### Summary
Briefly explain what the code does and give an overall assessment.
### Critical and High-Priority Issues
List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful.
### Other Findings
List medium- and low-priority issues, including maintainability and design concerns.
### Suggested Refactoring
Provide focused code changes or revised snippets. Preserve existing behavior unless a behavior change is explicitly justified.
### Testing Recommendations
Identify missing tests and describe specific test cases, including edge cases and failure scenarios.
### Positive Observations
Mention implementation choices that are clear, safe, efficient, or well designed. This is not fluff — it reinforces good patterns and tells the author what to keep doing.
### Final Verdict
Choose one:
- **Approve** — Ready to merge
- **Approve with minor changes** — Good to merge after addressing low/medium issues
- **Request changes** — Critical or high issues must be resolved before merge
## Change Sizing
Small, focused changes are easier to review, faster to merge, and safer to deploy.
```
~100 lines changed → Good. Reviewable in one sitting.
~300 lines changed → Acceptable if it's a single logical change.
~1000 lines changed → Too large. Split it.
```
**Watch file size, not just diff size.** Around 1000 *total* lines in a single file is a common inspection signal. When a change materially grows an already-large file, decompose first.
**Splitting strategies:**
| Strategy | How | When |
|----------|-----|------|
| **Stack** | Submit a small change, start the next one based on it | Sequential dependencies |
| **By file group** | Separate changes for groups needing different reviewers | Cross-cutting concerns |
| **Horizontal** | Create shared code/stubs first, then consumers | Layered architecture |
| **Vertical** | Break into smaller full-stack slices of the feature | Feature work |
**Separate refactoring from feature work.** A change that refactors and adds new behavior is two changes — submit them separately.
## Change Descriptions
- **First line:** Short, imperative, standalone. "Delete the FizzBuzz RPC" not "Deleting the FizzBuzz RPC."
- **Body:** What is changing and why. Include context and reasoning not visible in the code itself.
- **Anti-patterns:** "Fix bug," "Fix build," "Add patch," "Phase 1."
## Dependencies
Before adding any dependency:
1. Does the existing stack solve this? (Often it does.)
2. How large is the dependency? (Check bundle impact.)
3. Is it actively maintained? (Check last commit, open issues.)
4. Does it have known vulnerabilities? (`npm audit`)
5. What's the license? (Must be compatible with the project.)
**Rule:** Prefer standard library and existing utilities over new dependencies. Every dependency is a liability.
**Upgrading dependencies:**
- Read the changelog, not just the version number. Semver is a promise the maintainer may not have kept.
- One dependency per change. When a bulk bump breaks the build, you've lost which package did it.
- Let the tests decide — a green suite before *and* after, not just "it installed."
- Review the lockfile diff, not just `package.json`. Commit it and never hand-edit it.
For supply-chain risk triage, follow the `security-and-hardening` skill.
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "It works, that's good enough" | Working code that's unreadable, insecure, or architecturally wrong creates debt that compounds. |
| "I wrote it, so I know it's correct" | Authors are blind to their own assumptions. Every change benefits from another set of eyes. |
| "We'll clean it up later" | Later never comes. The review is the quality gate — use it. |
| "AI-generated code is probably fine" | AI code needs more scrutiny, not less. It's confident and plausible, even when wrong. |
| "The tests pass, so it's good" | Tests are necessary but not sufficient. They don't catch architecture, security, or readability problems. |
| "The refactor makes it cleaner" | Relocating complexity isn't reducing it. If the reader still holds the same number of concepts, the structure didn't improve. |
| "It's only a small addition to this file" | Small diffs still push files past healthy size and bolt branches onto unrelated flows. |
| "It's just a version bump" | A bump is a behavior change you didn't write. Read the changelog. |
| "I'll upgrade everything in one PR" | A bulk bump hides which package broke the build. One per change. |
| "It's duplicated but it's only two places" | Two becomes three becomes five. Extract now, before the copies diverge. |
| "The abstraction is future-proof" | YAGNI. Delete speculative generality — generalize on the third occurrence, not the first. |
| "It's clever but efficient" | Cleverness is a readability tax. If it needs a comment to understand, simplify it. |
## Red Flags
- PRs merged without any review
- Review that only checks if tests pass (ignoring other axes)
- "LGTM" without evidence of actual review
- Security-sensitive changes without security-focused review
- Large PRs that are "too big to review properly" (split them)
- No regression tests with bug fix PRs
- Accepting "I'll fix it later" — it never happens
- A refactor that moves code around without reducing the number of concepts a reader must hold
- New conditionals scattered into unrelated code paths (a missing abstraction)
- A bespoke helper that duplicates an existing canonical one
- A bulk "bump dependencies" PR with no changelog review
## Verification
After review is complete:
- [ ] All Critical issues are resolved
- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification
- [ ] Tests pass
- [ ] Build succeeds
- [ ] The verification story is documented (what changed, how it was verified)
- [ ] Dependency upgrades reviewed against changelog, isolated per package, verified by green suite
## Multi-Model Review Pattern
Use different models for different review perspectives:
```
Model A writes the code → Model B reviews → Model A addresses feedback → Human makes the final call
```
Different models have different blind spots.
## See Also
- For detailed security review guidance, see `security-and-hardening`
+47
View File
@@ -0,0 +1,47 @@
---
name: create-commit
description: Stage, review, and commit files following Penpot commit conventions.
---
# Skill: create-commit
Produce a git commit that follows Penpot's commit message conventions. This
skill owns the commit format, staging review, and safety checks — it does not
implement features or push.
## When to Use
- After code changes are complete and files need to be committed
- When delegated by a workflow step (e.g. implement-plan) to handle the commit
## Required Reading
Before drafting any commit, read `mem:workflow/creating-commits` end-to-end. It
is the authoritative source for the commit message format, the emoji menu,
subject/body limits, and the `AI-assisted-by` trailer. Follow it exactly.
## Workflow
1. **Stage the files** specified by the calling context. Do not ask for
confirmation.
2. Run `git diff --staged` to review the content. If you see secrets (API keys,
tokens, passwords, private keys, `.env` values), debug prints, or anything
that does not match the stated intent, **STOP** and tell the user before
committing.
3. Draft the message following the format in the memory doc, wrapping the body
at 72 characters per line, and run:
```bash
git commit -m "<subject>" -m "<body>"
```
(or `git commit -F -` if the body has unusual characters).
4. The `AI-assisted-by` trailer value is provided by the calling context — use
it verbatim.
## Constraints
- Do not push. Pushing is a separate workflow handled by the user.
- Do not run `git reset`, `git checkout`, `git restore`, `git clean`, or `rm`.
- Do not pass `--author`. Author identity comes from the local git config.
- Do not amend a commit you did not create in this session, unless explicitly asked.
- Do not bypass pre-commit hooks (`--no-verify`) unless explicitly asked.
- Do not add untracked files that were not created in this session.
+27
View File
@@ -0,0 +1,27 @@
---
name: create-issue
description: Create or update GitHub issues (from PR, from draft body, retitle existing). Routes to the canonical flow in `mem:workflow/creating-issues`.
---
# Skill: create-issue
Entry point for all GitHub issue work. All rules (title derivation, metadata,
body templates, Issue Type IDs), all flows, and all `gh` / GraphQL commands
live in `mem:workflow/creating-issues` (file:
`.serena/memories/workflow/creating-issues.md`). This skill routes to the
right flow.
## When to Use
- **Create from PR** — PR exists; the issue is the changelog/release unit,
the PR is the implementation. Issue = WHAT, PR = HOW.
→ memory section **Creating Issues from PRs**
- **Create from draft body** — Taiga story, user report, discussion; no PR
yet.
→ memory section **Creating Issues from Draft Body**
- **Retitle existing issue** — current title is vague, prefixed, or stale.
→ memory section **Retitling an Existing Issue**
Everything else (title derivation, metadata policy, body templates, Issue
Type IDs, create/verify/cleanup commands) lives in the memory — go to the
matching section there.
+39
View File
@@ -0,0 +1,39 @@
---
name: create-pr
description: Create or update a GitHub PR following Penpot conventions.
---
# Skill: create-pr
Create or update a GitHub PR. Read and follow:
- `mem:workflow/creating-prs` — title format, description structure, writing principles
- `mem:workflow/creating-commits` — commit type emojis
## When to Use
- Creating a new PR from a feature branch
- Updating an existing PR's title or description to match conventions
## Prerequisites
- `gh` CLI authenticated (`gh auth status`)
## Commands
**Create:**
```bash
gh pr create --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
```
**Update:**
```bash
gh pr edit <NUMBER> --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
```
**Verify:**
```bash
gh pr view <NUMBER> --repo penpot/penpot --json title,body
```
-230
View File
@@ -1,230 +0,0 @@
---
name: gh-issue-from-pr
description: Create a user-facing GitHub issue from a PR, separating the WHAT from the HOW, with correct milestone, project, labels, and issue type.
---
# Skill: gh-issue-from-pr
Create a GitHub issue that captures the **WHAT** (user-facing feature or
bug) from an existing PR that describes the **HOW** (implementation).
Used when the project board needs an issue as the primary changelog/release unit.
## When to Use
- Create a tracking issue from a PR for changelog purposes
- Extract the user-facing problem/feature from a PR's implementation details
- Assign milestone, project, labels, and issue type to a new issue derived from a PR
## Prerequisites
- `gh` CLI authenticated (`gh auth status`)
- Permission to create issues and edit PRs in the target repository
## Workflow
### 1. Understand the PR
```bash
gh pr view <PR_NUMBER> --repo penpot/penpot \
--json title,body,author,labels,baseRefName,mergedAt,state,milestone
```
Identify:
- **WHAT** — user-facing problem or feature. Goes into the issue.
Describe symptoms and impact, not internal mechanisms.
- **HOW** — implementation details. These belong in the PR, not the issue.
### 2. Determine metadata
| Field | Source | Rule |
|-------|--------|------|
| **Title** | PR title | Rewrite from user perspective. Strip leading emoji prefixes (`:bug:`, `:sparkles:`, `:tada:`). Focus on observable behavior. Use imperative mood. |
| **Labels** | PR labels | Copy user-facing labels (`bug`, `enhancement`, `community contribution`). Skip workflow labels (`backport candidate`, `team-qa`). |
| **Milestone** | PR milestone | **Always copy what's on the PR.** Fetch with: `gh pr view <PR_NUMBER> --json milestone --jq '.milestone.title'` If the PR has no milestone, create the issue without one. |
| **Project** | Always `Main` | Penpot uses the `Main` project (number 8) for all issues. |
| **Body** | PR's user-facing section | Extract steps to reproduce or feature description. Omit internal details. Use templates below. |
| **Issue Type** | PR labels / title | Map: `bug` label or `:bug:` title → `Bug`. `enhancement` label or `:sparkles:` title → `Enhancement`. Feature/epic → `Feature`. Default → `Task`. |
### 3. Write the issue body
**Bug template:**
```markdown
### Description
<what breaks, what the user experiences>
### Steps to reproduce
1. <step 1>
2. <step 2>
### Expected behavior
<what should happen instead>
### Affected versions
<version>
```
**Enhancement template:**
```markdown
### Description
<what the user can now do that they couldn't before>
### Use case
<why this is useful, who benefits>
### Affected versions
<version>
```
### 4. Create the issue
Write the body to a temp file to avoid shell quoting issues:
```bash
cat > /tmp/issue-body.md << 'ISSUE_BODY'
<body content here>
ISSUE_BODY
```
Create:
```bash
gh issue create \
--repo penpot/penpot \
--title "<Title>" \
--label "<label1>" \
--label "<label2>" \
--milestone "<milestone>" \
--project "Main" \
--body-file /tmp/issue-body.md
```
Output: `https://github.com/penpot/penpot/issues/<NUMBER>`
### 5. Assign to the PR author
Assign the issue to the PR author so they're responsible for it:
```bash
AUTHOR=$(gh pr view <PR_NUMBER> --repo penpot/penpot --json author --jq '.author.login')
gh issue edit <ISSUE_NUMBER> --repo penpot/penpot --add-assignee "$AUTHOR"
```
### 6. Set the Issue Type
`gh issue create` can't set the Issue Type directly. Use GraphQL.
Get the issue's GraphQL node ID:
```bash
ISSUE_ID=$(gh api graphql -f query='
query { repository(owner: "penpot", name: "penpot") {
issue(number: <ISSUE_NUMBER>) { id }
}}' --jq '.data.repository.issue.id')
```
Issue Type IDs for the Penpot repo:
| Type | ID |
|------|----|
| Bug | `IT_kwDOAcyBPM4AX5Nb` |
| Enhancement | `IT_kwDOAcyBPM4B_IQN` |
| Feature | `IT_kwDOAcyBPM4AX5Nf` |
| Task | `IT_kwDOAcyBPM4AX5NY` |
| Question | `IT_kwDOAcyBPM4B_IQj` |
| Docs | `IT_kwDOAcyBPM4B_IQz` |
Set it:
```bash
gh api graphql -f query='
mutation {
updateIssue(input: {
id: "'"$ISSUE_ID"'"
issueTypeId: "<TYPE_ID>"
}) {
issue { number issueType { name } }
}
}'
```
### 7. Verify
```bash
gh issue view <ISSUE_NUMBER> --repo penpot/penpot \
--json title,milestone,projectItems,labels \
--jq '{title, milestone: .milestone.title, projects: [.projectItems[].title], labels: [.labels[].name]}'
gh api graphql -f query='
query { repository(owner: "penpot", name: "penpot") {
issue(number: <ISSUE_NUMBER>) { issueType { name } }
}}' --jq '.data.repository.issue.issueType.name'
```
### 8. Link the PR to the issue
Append `Closes #<ISSUE_NUMBER>` to the PR body:
```bash
gh pr view <PR_NUMBER> --repo penpot/penpot --json body --jq '.body' > /tmp/pr-body.md
printf "\n\nCloses #<ISSUE_NUMBER>\n" >> /tmp/pr-body.md
gh pr edit <PR_NUMBER> --repo penpot/penpot --body-file /tmp/pr-body.md
# Verify
gh pr view <PR_NUMBER> --repo penpot/penpot --json body \
--jq '.body | test("Closes #<ISSUE_NUMBER>")'
```
**Note:** If the PR is already merged, `Closes` won't auto-close the issue
— it only creates the "Development" sidebar link. This is the desired
behavior since the issue is a tracking artifact.
### 9. Clean up
```bash
rm -f /tmp/issue-body.md /tmp/pr-body.md
```
## Label rules
| PR has | Issue gets |
|--------|-----------|
| `bug` | `bug` |
| `enhancement` | `enhancement` |
| `community contribution` | `community contribution` |
| `backport candidate` | *(skip — workflow label)* |
| `team-qa` | *(skip — workflow label)* |
| No user-facing label | Infer from title: `:bug:``bug`, `:sparkles:``enhancement` |
## Issue Type mapping
| PR label(s) / title prefix | Issue Type |
|----------------------------|-----------|
| `bug` or `:bug:` | Bug |
| `enhancement` or `:sparkles:` or `:tada:` | Enhancement |
| Feature / epic | Feature |
| Documentation | Docs |
| None of the above | Task |
## Key Principles
- **Issue = WHAT, PR = HOW.** Never put implementation details in the
issue body. The issue is for users, QA, and changelog readers.
- **Copy the milestone from the PR.** Don't guess based on branch names.
If the PR has no milestone, create the issue without one.
- **Set Issue Type via GraphQL** — `gh issue create` can't set it.
- **Link via PR body** — `Closes #<NUMBER>` creates the "Development"
sidebar link automatically.
- **One issue per PR** — even if a PR fixes multiple things, create a
single issue that summarizes the overall change.
- **Community attribution:** if the PR has the `community contribution`
label or the author is not a core team member, add the label to the issue.
-123
View File
@@ -1,123 +0,0 @@
---
name: issue-title
description: Derive a clear, well-formatted title for a GitHub issue from its description body, using descriptive present-tense for bugs and imperative mood for features, always including the "where" (location in the UI/module).
---
# Skill: issue-title
Derive a concise, descriptive title for a GitHub issue based on its body
content. Use **descriptive present tense for bugs** (e.g. "Plugin API
crashes when setting text fills") and **imperative mood for features** (e.g.
"Add customizable dash and gap controls"). No emoji or type prefixes
(`feat:`, `bug:`, `feature:`, etc.).
Can be used both when **creating a new issue** and when **updating an
existing one** that has a vague or outdated title.
## When to Use
- Creating a new issue and need a well-formatted title from the draft body
- An existing issue has a vague, outdated, or auto-generated title (e.g.
`[PENPOT FEEDBACK]: ...`, `feature: ...`)
- The current title doesn't reflect the actual content of the description
- The title is missing the "where" (which part of the UI/module is affected)
## Prerequisites
- `gh` CLI authenticated (`gh auth status`)
## Workflow
### 1. Get the issue body
For an **existing issue**, fetch it:
```bash
gh issue view <NUMBER> --repo penpot/penpot --json title,body
```
For a **new issue**, read the draft body from wherever it was provided
(Taiga link, user report, discussion, etc.).
### 2. Read the body and derive a title
Extract the core problem or request from the description. Distinguish between
bug reports and feature requests:
**Bug titles (descriptive, present tense):**
Describe the symptom as it appears to the user. Format:
`[Where] [present-tense verb] when [condition]`
- *"Plugin API crashes when setting text fills"*
- *"Canvas renders glitches when zooming quickly"*
- *"French Canada locale falls back to French (fr) translations"*
- *"Text layer content is not deleted when WebGL render is enabled"*
Do **not** start bug titles with "Fix" or any imperative verb. The title
should state what's broken, not command a fix.
**Feature / Enhancement titles (imperative mood):**
Command what should be built. Format:
`[Imperative verb] [what] in/on [where]`
- *"Add customizable dash and gap length controls to dashed strokes in the sidebar"*
- *"Show user, timestamp, and hash in the workspace history panel like git commits"*
- *"Validate shape on add-object to catch malformed inputs early"*
**Universal rules (both types):**
- **Include the "where"** — specify the UI location or module (e.g.
"in the sidebar", "in the workspace history panel", "on the stroke
options")
- **No prefixes** — strip `bug:`, `feature:`, `feat:`, `:bug:`, `:sparkles:`,
`[PENPOT FEEDBACK]`, etc.
- **No emoji** — plain text only
- **Be specific** — prefer concrete detail over generality. If the
description mentions two related problems, capture both.
**Examples:**
| Original / draft title | Type | New title |
|---|---|---|
| `[PENPOT FEEDBACK]: WebGL` | Bug | `Canvas renders glitches when zooming quickly — text appears distorted and nodes have background-colored rectangles` |
| `bug: flatten-nested-tokens-json uses $type instead of $value as the DTCG token/group discriminator` | Bug | `Token import fails when group-level type inheritance is used — parser misidentifies groups as tokens` |
| `feature: Dashed stroke customization` | Feature | `Add customizable dash and gap length controls to dashed strokes in the sidebar` |
| `feature: Add more detail to history of actions` | Feature | `Show user, timestamp, and hash in the workspace history panel like git commits` |
### 3. Apply the title
**If updating an existing issue:**
```bash
gh issue edit <NUMBER> --repo penpot/penpot --title "<NEW TITLE>"
```
**If creating a new issue:**
```bash
gh issue create --repo penpot/penpot --title "<NEW TITLE>" --body "<BODY>"
```
### 4. Confirm
For updates, the command returns the issue URL. Verify by optionally fetching
again:
```bash
gh issue view <NUMBER> --repo penpot/penpot --json title
```
## Key Principles
- **Bug titles describe the symptom** — present tense, 3rd person:
"crashes", "fails", "shows", "is cut off", "does not load". Do not
start with "Fix" or "Bug:".
- **Feature titles use imperative mood** — command form: "Add", "Show",
"Use", "Validate", "Support", "Toggle".
- **Always include the "where"** — a title like "Crashes when zooming"
is too vague; "Canvas crashes when zooming quickly" is clear.
- **No prefixes, no emoji** — strip all type labels and decorative
characters from the title.
- **Derive from the body, not the current title** — the body contains
the real detail; the current title may be auto-generated or stale.
- **Two problems → cover both** — if the description has two distinct
but related issues, capture both in the title joined by "and".
+12 -95
View File
@@ -1,31 +1,25 @@
---
name: nrepl-eval
description: Evaluate Clojure code via nREPL using the standalone tools/nrepl-eval.mjs CLI tool.
description: Evaluate Clojure code via nREPL using the standalone scripts/nrepl-eval.mjs CLI tool.
---
# nREPL Eval
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
`tools/nrepl-eval.mjs` — a standalone CLI application.
`scripts/nrepl-eval.mjs`.
Session state (defs, in-ns, etc.) persists across invocations via a stored
session ID, so you can build up state incrementally.
Full documentation: `mem:scripts/nrepl-eval` (file: `.serena/memories/scripts/nrepl-eval.md`)
## Usage
## Quick Reference
```bash
node tools/nrepl-eval.mjs [options] [<code>]
./scripts/nrepl-eval.mjs [options] [<code>]
```
The tool is also executable directly:
```bash
./tools/nrepl-eval.mjs [options] [<code>]
```
## Options
| Flag | Description | Default |
|------|-------------|---------|
| `--backend` | Connect to backend nREPL (port 6064) | — |
| `--frontend` | Connect to frontend nREPL (port 3447) | — |
| `-p, --port PORT` | nREPL server port | `6064` |
| `-H, --host HOST` | nREPL server host | `127.0.0.1` |
| `-t, --timeout MS` | Timeout in milliseconds | `120000` |
@@ -33,88 +27,11 @@ The tool is also executable directly:
| `-e, --last-error` | Evaluate `*e` to retrieve the last exception | — |
| `-h, --help` | Show help message | — |
## When to Use
Use this tool when you need to:
1. **Evaluate Clojure code** during development — test functions, inspect
state, or run experiments against a running Clojure process.
2. **Verify that edited files compile** — require namespaces with `:reload`
to pick up changes.
3. **Inspect the last exception** after a failed evaluation — use `-e` to
print the error stored in `*e`.
## Workflow
### 1. Session management
Sessions are persisted to `/tmp/penpot-nrepl-session-<host>-<port>`. State
carries across calls automatically:
## Examples
```bash
./tools/nrepl-eval.mjs '(def x 42)'
./tools/nrepl-eval.mjs 'x'
# => 42
./scripts/nrepl-eval.mjs '(+ 1 2 3)'
./scripts/nrepl-eval.mjs --backend '(+ 1 2 3)'
./scripts/nrepl-eval.mjs --frontend '(js/alert "hi")'
./scripts/nrepl-eval.mjs -e
```
Reset the session to start fresh:
```bash
./tools/nrepl-eval.mjs --reset-session '(def x 0)'
```
### 2. Evaluate code
**Single expression (inline) — uses default port 6064:**
```bash
./tools/nrepl-eval.mjs '(+ 1 2 3)'
```
**Multiple expressions via heredoc (recommended — avoids escaping issues):**
```bash
./tools/nrepl-eval.mjs <<'EOF'
(def x 10)
(+ x 20)
EOF
```
**Override with a different port:**
```bash
./tools/nrepl-eval.mjs -p 7888 '(+ 1 2 3)'
```
### 3. Inspect last exception
After code throws an error, retrieve the full exception details:
```bash
./tools/nrepl-eval.mjs -e
```
## Common Patterns
**Require a namespace with reload:**
```bash
./tools/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)"
```
**Test a function:**
```bash
./tools/nrepl-eval.mjs "(ns/my-function arg1 arg2)"
```
**Long-running operation with custom timeout:**
```bash
./tools/nrepl-eval.mjs -t 300000 "(long-running-fn)"
```
## Key Principles
- **Default port is 6064** — just pass code directly, no `-p` needed when
your nREPL server is on 6064. Use `-p <PORT>` for a different port.
- **Always use `:reload`** when requiring namespaces to pick up file changes.
- **Session is reused** across invocations — defs, in-ns, and var bindings
persist. Use `--reset-session` to clear.
- **Do not start any server** — the tool connects to an existing nREPL
server, it is not the agent's responsibility to start the nREPL server
(assume the server is already running on the specified port).
+271
View File
@@ -0,0 +1,271 @@
---
name: planner
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to .opencode/plans/YYYY-MM-DD-<title>.md.
---
# Planner
Read-only senior software architect role for Penpot. Produces structured
implementation plans that engineers or other agents can execute. Never writes
or modifies code.
## When to Use
- The user asks for a plan, design, or analysis of a feature or bug.
- The user wants to understand which parts of the codebase a task will touch.
- The user needs a step-by-step implementation plan with file paths, function
names, and test strategy.
- The user asks "how would I implement X?" or "what's involved in fixing Y?".
- The user is about to start non-trivial work and wants a bite-sized task
breakdown.
Do **not** use this skill to actually implement anything — it is read-only.
## Role
You are a Senior Software Architect working on Penpot, an open-source design
tool. Your sole responsibility is planning and analysis — you do NOT write or
modify code.
You help users understand the codebase, design solutions, and create detailed
implementation plans that other agents or developers can execute. Document
everything they need to know: which files to touch for each task, code patterns,
tests, and how to verify correctness. Apply DRY and KISS principles.
Do **not** suggest commit messages or commit names anywhere in your plans or
responses — committing is the developer's responsibility.
## Required Reading Before Planning
Before drafting any plan, work through the project's own guidance:
1. Read `critical-info` (`.serena/memories/critical-info.md`) — the entry point
that describes the monorepo structure and module dependency graph.
2. From `critical-info`, identify which modules your task affects.
3. Read each affected module's core memory, e.g. `mem:frontend/core`,
`mem:backend/core`, `mem:common/core`, `mem:exporter/core`,
`mem:render-wasm/core`. Follow `mem:` references deeper as needed.
4. For each affected module, note its lint, format, and test commands so the
plan can include concrete verification steps.
Skipping this step is the #1 cause of incorrect or incomplete plans.
## The Planning Process
### Phase 1: Architecture Analysis
1. Read the spec, requirements, or feature request.
2. Analyze the codebase architecture and identify affected modules.
3. Read project conventions (starting with `critical-info` and module core
memories) before drafting.
4. Map dependencies between components (see the dependency graph in
`critical-info`).
5. Identify risks, edge cases, performance implications, and breaking changes.
### Phase 2: Task Breakdown
Implementation order follows the monorepo's dependency graph:
`frontend -> common`, `backend -> common`, `exporter -> common`,
`frontend -> render-wasm`. Build shared foundations first, then layer
consumers on top.
#### Slice Vertically
Instead of building all of common, then all of backend, then all of frontend —
build one complete feature path at a time:
```
Task 1: common data types + schema ← foundation
Task 2: backend RPC handler + persistence
Task 3: frontend UI component + API integration
```
Each vertical slice delivers working, testable functionality.
#### Write Tasks
Each task follows this structure:
```markdown
## Task [N]: [Short descriptive title]
**Description:** One paragraph explaining what this task accomplishes.
**Acceptance criteria:**
- [ ] [Specific, testable condition]
- [ ] [Specific, testable condition]
**Verification:**
- [ ] Tests pass (module-specific test command)
- [ ] Lint/formatter passes (module-specific check command)
**Dependencies:** [Task numbers this depends on, or "None"]
**Files likely touched:**
- `path/to/file.clj`
- `path/to/file_test.clj`
```
Replace "module-specific test command" with the actual commands for the module
(e.g. `clojure -M:dev:test` for backend/common, `npx shadow-cljs compile test && npx karma start` for frontend,
or the commands noted in the module's core memory).
#### Estimate Scope
| Size | Files | Scope |
|------|-------|-------|
| **XS** | 1 | Single function, config change, or schema tweak |
| **S** | 1-2 | One handler or component method |
| **M** | 3-5 | One vertical feature slice |
| **L** | 5-8 | Multi-component feature |
| **XL** | 8+ | **Too large — break it down further** |
If a task is L or larger, break it into smaller tasks. Agents perform best on
S and M tasks.
**When to break a task down further:**
- It would take more than one focused session
- You cannot describe the acceptance criteria in 3 or fewer bullet points
- It touches two or more independent subsystems
- You find yourself writing "and" in the task title (a sign it is two tasks)
#### Order and Checkpoints
Arrange tasks so that:
1. Dependencies are satisfied (build foundation first)
2. Each task leaves the system in a working state
3. Verification checkpoints occur after every 2-3 tasks
4. High-risk tasks are early (fail fast)
Add explicit checkpoints with the relevant module commands:
```markdown
## Checkpoint: After Tasks 1-3
- [ ] All tests pass (module-specific command)
- [ ] Lint/format passes (module-specific command)
- [ ] Core flow works end-to-end
- [ ] Review with human before proceeding
```
## Requirements
- Analyze the codebase architecture and identify affected modules.
- Read project conventions before drafting (start with `critical-info` and
affected module core memories).
- Break down complex features or bugs into atomic, actionable steps.
- Propose solutions with clear rationale, trade-offs, and sequencing.
- Identify risks, edge cases, performance implications, and breaking changes.
- Apply DRY and KISS principles to the proposed implementation.
- Define a testing strategy aligned with each affected module's tooling.
- Every task must have acceptance criteria and verification steps.
- Checkpoints must exist between major phases.
## Constraints
- You are **analysis-only** — never create, edit, or delete source code.
- The only file write you may attempt is the plan itself, saved to
`.opencode/plans/`.
- You do **not** run builds, tests, linters, or any commands that modify state.
- You do **not** create git commits or interact with version control.
- You do **not** execute shell commands beyond read-only searches.
- Your output is a structured plan or analysis, ready for handoff to an
engineer agent or developer.
## Output Format
The plan is always delivered in the response so the user sees it regardless
of which agent is running the skill.
Additionally, save the plan to:
```
.opencode/plans/YYYY-MM-DD-<plan-one-line-title>.md
```
Use today's date in the user's local timezone. The `<plan-one-line-title>`
slug is lowercase, hyphen-separated, and a short summary of the task
(e.g. `add-batch-get-profiles-for-file-comments`). Create the
`.opencode/plans/` directory if it does not exist.
Always attempt the write. If the user explicitly provides a target file path,
use that path instead of the default.
### Plan Document Template
```markdown
# Plan: [Feature/Project Name]
## Context
[One paragraph: what is the problem or feature request? Why is it needed?]
## Affected Modules
[Which modules of the monorepo are involved? Reference module paths and any
`mem:` memories that were consulted.]
## Architecture Decisions
- [Key decision 1 and rationale]
- [Key decision 2 and rationale]
## Risks & Considerations
[Edge cases, performance implications, breaking changes, migration concerns,
security implications.]
## Approach
[Step-by-step implementation plan with file paths, function names, and code
shape where applicable. Group steps into atomic, ordered tasks.]
## Task List
### Phase 1: Foundation
- [ ] Task 1: ...
- [ ] Task 2: ...
### Checkpoint: Phase 1
- [ ] Tests pass, lint/formatter clean (module-specific commands)
### Phase 2: Core Features
- [ ] Task 3: ...
- [ ] Task 4: ...
### Checkpoint: Phase 2
- [ ] End-to-end flow works
### Phase 3: Polish
- [ ] Task 5: ...
- [ ] Task 6: ...
### Checkpoint: Complete
- [ ] All acceptance criteria met
- [ ] Ready for review
## Testing Strategy
[How to verify: which test commands to run per module, what cases to cover,
manual verification steps, lint/format checks. Consult each module's core
memory for the exact commands.]
## Parallelization Opportunities
- **Safe to parallelize:** Independent feature slices across separate
modules, tests for already-implemented features
- **Must be sequential:** Shared common schema changes, database migrations
- **Needs coordination:** Features that share a contract (define the contract
first, then parallelize)
## Open Questions
- [Question needing human input]
```
When the plan is purely analytical (e.g. a code review or feasibility study
with no implementation), skip the **Approach** and **Task List** sections and
lead with **Findings** instead, keeping the rest of the structure.
## Verification Checklist
Before starting implementation, confirm:
- [ ] Every task has acceptance criteria
- [ ] Every task has a verification step
- [ ] Task dependencies are identified and ordered correctly
- [ ] No task touches more than ~5 files
- [ ] Checkpoints exist between major phases
- [ ] The human has reviewed and approved the plan
+134
View File
@@ -0,0 +1,134 @@
---
name: refine-prompt
description: Refine and improve a user-supplied prompt for maximum clarity and effectiveness using prompt-engineering best practices and Penpot project context. Outputs a rewritten prompt (and brief rationale); never executes the prompt.
---
# Refine Prompt
Expert prompt-engineering pass on a user-supplied prompt. Takes a draft prompt
and returns a clearer, more effective, well-structured version — ready to be
used with any AI model. Never executes the prompt itself.
## When to Use
- The user shares a prompt and asks to improve, refine, polish, or rewrite it.
- The user asks "make this prompt better" or "can you clean this up?".
- The user wants to add structure, constraints, examples, or output format to
a vague prompt.
- The user wants a prompt adapted for a specific target model, audience, or
task type.
Do **not** use this skill to actually answer the prompt or do the task — it
only rewrites the prompt.
## Role
You are an expert Prompt Engineer with strong knowledge of Penpot. Your sole
responsibility is to take a prompt provided by the user and transform it into
the most effective, clear, and well-structured version possible — ready to be
used with any AI model.
You do **not** execute tasks. You do **not** write code. You only design and
refine prompts.
## Required Reading Before Refining
Before rewriting, internalize the project context the prompt will likely run
against:
1. Read `AGENTS.md` (root) for the project-level rules and conventions.
2. Read `.serena/memories/critical-info.md` (or the equivalent entry point) to
understand the module layout (`frontend`, `backend`, `common`,
`render-wasm`, `exporter`, `mcp`, `plugins`, `library`).
3. Skim the relevant module's core memory (`mem:frontend/core`,
`mem:backend/core`, etc.) when the prompt targets a specific module — this
lets you inject precise vocabulary, file conventions, and test commands
into the refined prompt.
This step matters most when the user is preparing a prompt *about* the
Penpot codebase. For generic prompts, focus on prompt-engineering principles
and only weave in Penpot context when it is clearly relevant.
## Requirements
- Analyze the original prompt: identify its intent, target audience,
ambiguities, missing context, and structural weaknesses.
- Ask clarifying questions if the intent is unclear or if critical information
is missing (e.g. target model, expected output format, tone, constraints).
Keep questions concise and grouped. Prefer to ask 14 questions at once
rather than one at a time. **Use the `question` tool** to ask them so the
user gets a structured multi-choice UI; reserve a plain `## Clarifying
questions` markdown section for cases where the `question` tool is
unavailable or the question is genuinely open-ended.
- Rewrite the prompt using prompt-engineering best practices (see below).
- Preserve the user's original intent — do not change the underlying task.
- When the user provides Penpot project context, weave in the relevant
conventions, module paths, and tooling.
## Prompt Engineering Principles
Apply these techniques when refining prompts:
- **Be specific and explicit**: Replace vague instructions with precise ones.
- **Set the context**: Include background information the model needs to
perform well.
- **Specify the output format**: State the desired structure, length, tone,
or format (e.g. bullet list, JSON, step-by-step).
- **Add constraints**: Include what the model should avoid or not do.
- **Use examples** (few-shot): When applicable, suggest adding examples to
anchor the model's behaviour.
- **Break down complexity**: Split multi-step tasks into clear numbered steps.
- **Avoid ambiguity**: Remove pronouns and references that could be
misinterpreted.
- **Chain of thought**: For reasoning tasks, include "Think step by step."
- **Role framing**: When helpful, give the model a clear role
("You are a senior backend engineer...").
- **Tool awareness**: When the prompt targets an agentic model, mention
relevant tools (`grep`, `glob`, `read`, `bash`, etc.) so the model uses the
right surface.
## Constraints
- Do **not** execute the prompt yourself.
- Do **not** answer the question inside the prompt.
- Do **not** add unnecessary verbosity — prompts should be as short as they
can be while remaining complete.
- Always preserve the user's original intent.
- If the user provides Penpot project context, prefer Penpot-specific
vocabulary over generic terms (e.g. name actual modules and `mem:`
references instead of "the codebase").
## Output Format
Deliver the result in the response as two clearly separated blocks:
1. **Refined prompt** — a single fenced code block (markdown ```) containing
the rewritten prompt, ready to copy and use.
2. **What changed (brief)** — a short bulleted list of the most important
changes you made and why (37 bullets max). Skip the rationale if the
changes are trivial.
If you asked clarifying questions via the `question` tool, stop and wait for
the answers before producing a refined prompt. If the `question` tool was not
available and you asked the questions in chat, list them in a separate
**Clarifying questions** section above the refined prompt and stop — do not
produce a refined prompt until the user answers. If the user explicitly told
you to proceed without questions (e.g. "just rewrite it"), make reasonable
assumptions and note them under **Assumptions made** in the rationale block.
## File Persistence
Always persist the refined prompt to disk so it can be re-used later, versioned
in git, and shared with other agents. The response still contains the prompt
and rationale blocks; the file is an additional artifact, not a replacement.
- Save the refined prompt (the body inside the fenced code block, **without**
the surrounding ``` fences) to `.opencode/prompts/<descriptive-name>.md`.
- Use a **kebab-case** filename that summarises the task, e.g.
`add-error-reports-management-rpc.md`, `backend-rpc-security-audit.md`. No
spaces, no uppercase, no version numbers or dates in the filename.
- If `.opencode/prompts/` does not exist, create it before writing.
- If a file with the same name already exists, overwrite it (the file is the
refined prompt, not a log).
- Only skip the file write when the user explicitly opts out (e.g. "don't save
this one", "just show it in the chat"). When in doubt, save it.
@@ -0,0 +1,457 @@
---
name: security-and-hardening
description: Hardens code against vulnerabilities. Use when handling user input, authentication, data storage, or external integrations. Use when building any feature that accepts untrusted data, manages user sessions, or interacts with third-party services.
---
# Security and Hardening
## Overview
Security-first development practices for web applications. Treat every external input as hostile, every secret as sacred, and every authorization check as mandatory. Security isn't a phase — it's a constraint on every line of code that touches user data, authentication, or external systems.
## When to Use
- Building anything that accepts user input
- Implementing authentication or authorization
- Storing or transmitting sensitive data
- Integrating with external APIs or services
- Adding file uploads, webhooks, or callbacks
- Handling payment or PII data
## Process: Threat Model First
Controls bolted on without a threat model are guesses. Before hardening, spend five minutes thinking like an attacker:
1. **Map the trust boundaries.** Where does untrusted data cross into your system? HTTP requests, form fields, file uploads, webhooks, third-party APIs, message queues, and **LLM output**. Every boundary is attack surface.
2. **Name the assets.** What's worth stealing or breaking? Credentials, PII, payment data, admin actions, money movement.
3. **Run STRIDE over each boundary** — a quick lens, not a ceremony:
| Threat | Ask | Typical mitigation |
|---|---|---|
| **S**poofing | Can someone impersonate a user/service? | Authentication, signature verification |
| **T**ampering | Can data be altered in transit or at rest? | Integrity checks, parameterized queries, HTTPS |
| **R**epudiation | Can an action be denied later? | Audit logging of security events |
| **I**nformation disclosure | Can data leak? | Encryption, field allowlists, generic errors |
| **D**enial of service | Can it be overwhelmed? | Rate limiting, input size caps, timeouts |
| **E**levation of privilege | Can a user gain rights they shouldn't? | Authorization checks, least privilege |
4. **Write abuse cases next to use cases.** For each feature, ask "how would I misuse this?" — then make that your first test.
If you can't name the trust boundaries for a feature, you're not ready to secure it. This is OWASP **A04: Insecure Design** — most breaches begin in design, not code.
## The Three-Tier Boundary System
### Always Do (No Exceptions)
- **Validate all external input** at the system boundary (API routes, form handlers)
- **Parameterize all database queries** — never concatenate user input into SQL
- **Encode output** to prevent XSS (use framework auto-escaping, don't bypass it)
- **Use HTTPS** for all external communication
- **Hash passwords** with bcrypt/scrypt/argon2 (never store plaintext)
- **Set security headers** (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- **Use httpOnly, secure, sameSite cookies** for sessions
- **Run `npm audit`** (or equivalent) before every release
### Ask First (Requires Human Approval)
- Adding new authentication flows or changing auth logic
- Storing new categories of sensitive data (PII, payment info)
- Adding new external service integrations
- Changing CORS configuration
- Adding file upload handlers
- Modifying rate limiting or throttling
- Granting elevated permissions or roles
### Never Do
- **Never commit secrets** to version control (API keys, passwords, tokens)
- **Never log sensitive data** (passwords, tokens, full credit card numbers)
- **Never trust client-side validation** as a security boundary
- **Never disable security headers** for convenience
- **Never use `eval()` or `innerHTML`** with user-provided data
- **Never store sessions in client-accessible storage** (localStorage for auth tokens)
- **Never expose stack traces** or internal error details to users
## OWASP Top 10 Prevention Patterns
These are prevention patterns, not a ranking. For the 2021 ordering, see the quick-reference table in `references/security-checklist.md`.
### Injection (SQL, NoSQL, OS Command)
```typescript
// BAD: SQL injection via string concatenation
const query = `SELECT * FROM users WHERE id = '${userId}'`;
// GOOD: Parameterized query
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// GOOD: ORM with parameterized input
const user = await prisma.user.findUnique({ where: { id: userId } });
```
### Broken Authentication
```typescript
// Password hashing
import { hash, compare } from 'bcrypt';
const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);
// Session management
app.use(session({
secret: process.env.SESSION_SECRET, // From environment, not code
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Not accessible via JavaScript
secure: true, // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
}));
```
### Cross-Site Scripting (XSS)
```typescript
// BAD: Rendering user input as HTML
element.innerHTML = userInput;
// GOOD: Use framework auto-escaping (React does this by default)
return <div>{userInput}</div>;
// If you MUST render HTML, sanitize first
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
```
### Broken Access Control
```typescript
// Always check authorization, not just authentication
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
const task = await taskService.findById(req.params.id);
// Check that the authenticated user owns this resource
if (task.ownerId !== req.user.id) {
return res.status(403).json({
error: { code: 'FORBIDDEN', message: 'Not authorized to modify this task' }
});
}
// Proceed with update
const updated = await taskService.update(req.params.id, req.body);
return res.json(updated);
});
```
### Security Misconfiguration
```typescript
// Security headers (use helmet for Express)
import helmet from 'helmet';
app.use(helmet());
// Content Security Policy
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Tighten if possible
imgSrc: ["'self'", 'data:', 'https:'],
connectSrc: ["'self'"],
},
}));
// CORS — restrict to known origins
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
credentials: true,
}));
```
### Sensitive Data Exposure
```typescript
// Never return sensitive fields in API responses
function sanitizeUser(user: UserRecord): PublicUser {
const { passwordHash, resetToken, ...publicFields } = user;
return publicFields;
}
// Use environment variables for secrets
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY not configured');
```
### Server-Side Request Forgery (SSRF)
Any time the server fetches a URL the user influenced — webhooks, "import from URL", image proxies, link previews — an attacker can aim it at internal services (cloud metadata, `localhost`, private IPs).
```typescript
// BAD: fetch whatever the user gives you
await fetch(req.body.webhookUrl);
// GOOD: allowlist scheme + host, reject if ANY resolved IP is private, forbid redirects
import { lookup } from 'node:dns/promises';
import ipaddr from 'ipaddr.js';
const ALLOWED_HOSTS = new Set(['hooks.example.com']);
async function assertSafeUrl(raw: string): Promise<URL> {
const url = new URL(raw);
if (url.protocol !== 'https:') throw new Error('https only');
if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error('host not allowed');
// Resolve ALL records; a single private/reserved address fails the check.
const addrs = await lookup(url.hostname, { all: true });
if (addrs.some((a) => ipaddr.parse(a.address).range() !== 'unicast')) {
throw new Error('private/reserved IP');
}
return url;
}
await fetch(await assertSafeUrl(req.body.webhookUrl), { redirect: 'error' });
```
The `range() !== 'unicast'` check covers loopback, link-local `169.254.169.254` (cloud metadata, the #1 SSRF target), private, and unique-local ranges across IPv4 and IPv6.
**Caveat — this still has a TOCTOU gap.** `fetch` resolves DNS again after the check, so an attacker using a short-TTL record can rebind to an internal IP between validation and connection. For high-risk surfaces, resolve once and connect to the pinned IP, or put a filtering agent in front (`request-filtering-agent` / `ssrf-req-filter`).
## Input Validation Patterns
### Schema Validation at Boundaries
```typescript
import { z } from 'zod';
const CreateTaskSchema = z.object({
title: z.string().min(1).max(200).trim(),
description: z.string().max(2000).optional(),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
dueDate: z.string().datetime().optional(),
});
// Validate at the route handler
app.post('/api/tasks', async (req, res) => {
const result = CreateTaskSchema.safeParse(req.body);
if (!result.success) {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: result.error.flatten(),
},
});
}
// result.data is now typed and validated
const task = await taskService.create(result.data);
return res.status(201).json(task);
});
```
### File Upload Safety
```typescript
// Restrict file types and sizes
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
function validateUpload(file: UploadedFile) {
if (!ALLOWED_TYPES.includes(file.mimetype)) {
throw new ValidationError('File type not allowed');
}
if (file.size > MAX_SIZE) {
throw new ValidationError('File too large (max 5MB)');
}
// Don't trust the file extension — check magic bytes if critical
}
```
## Triaging npm audit Results
Not all audit findings require immediate action. Use this decision tree:
```
npm audit reports a vulnerability
├── Severity: critical or high
│ ├── Is the vulnerable code reachable in your app?
│ │ ├── YES --> Fix immediately (update, patch, or replace the dependency)
│ │ └── NO (dev-only dep, unused code path) --> Fix soon, but not a blocker
│ └── Is a fix available?
│ ├── YES --> Update to the patched version
│ └── NO --> Check for workarounds, consider replacing the dependency, or add to allowlist with a review date
├── Severity: moderate
│ ├── Reachable in production? --> Fix in the next release cycle
│ └── Dev-only? --> Fix when convenient, track in backlog
└── Severity: low
└── Track and fix during regular dependency updates
```
**Key questions:**
- Is the vulnerable function actually called in your code path?
- Is the dependency a runtime dependency or dev-only?
- Is the vulnerability exploitable given your deployment context (e.g., a server-side vulnerability in a client-only app)?
When you defer a fix, document the reason and set a review date.
### Supply-Chain Hygiene
`npm audit` catches known CVEs; it won't catch a malicious or typosquatted package. Also:
- **Commit the lockfile** and install with `npm ci` (not `npm install`) in CI — reproducible builds, no silent version drift.
- **Review new dependencies before adding them** — maintenance, download counts, and whether they truly earn their place. Every dependency is attack surface (OWASP **A06: Vulnerable Components**, **LLM03: Supply Chain**).
- **Be wary of `postinstall` scripts** in unfamiliar packages — they run arbitrary code at install time.
- **Watch for typosquats** — `cross-env` vs `crossenv`, `react-dom` vs `reactdom`.
## Rate Limiting
```typescript
import rateLimit from 'express-rate-limit';
// General API rate limit
app.use('/api/', rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
standardHeaders: true,
legacyHeaders: false,
}));
// Stricter limit for auth endpoints
app.use('/api/auth/', rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // 10 attempts per 15 minutes
}));
```
## Secrets Management
```
.env files:
├── .env.example → Committed (template with placeholder values)
├── .env → NOT committed (contains real secrets)
└── .env.local → NOT committed (local overrides)
.gitignore must include:
.env
.env.local
.env.*.local
*.pem
*.key
```
**Always check before committing:**
```bash
# Check for accidentally staged secrets
git diff --cached | grep -i "password\|secret\|api_key\|token"
```
**If a secret is ever committed, rotate it.** Deleting the line or rewriting history is not enough — assume it's compromised the moment it reaches a remote. Revoke and reissue the key first, then purge it from history.
## Securing AI / LLM Features
If your app calls an LLM — chatbots, summarizers, agents, RAG — it inherits a new attack surface. Map it to the [OWASP Top 10 for LLM Applications (2025)](https://genai.owasp.org/llm-top-10/):
- **Treat all model output as untrusted input (LLM05: Improper Output Handling).** Never pass LLM output straight into `eval`, SQL, a shell, `innerHTML`, or a file path. Validate and encode it exactly as you would raw user input.
- **Assume prompts can be hijacked (LLM01: Prompt Injection).** Untrusted text in the context window — a user message, a fetched web page, a PDF — can carry instructions. The system prompt is not a security boundary; enforce permissions in code, not in the prompt.
- **Keep secrets and other users' data out of prompts (LLM02 / LLM07).** Anything in the context can be echoed back. Don't put API keys, cross-tenant data, or the full system prompt where the model can repeat it.
- **Constrain tool and agent permissions (LLM06: Excessive Agency).** Scope tools to the minimum, require confirmation for destructive or irreversible actions, and validate every tool argument.
- **Bound consumption (LLM10: Unbounded Consumption).** Cap tokens, request rate, and loop/recursion depth so a crafted input can't run up cost or hang the system.
- **Isolate retrieval data (LLM08: Vector and Embedding Weaknesses).** In RAG, treat the vector store as a trust boundary: partition embeddings per tenant so one user can't retrieve another's data, and validate documents before indexing so poisoned content can't steer answers.
```typescript
// BAD: trusting model output as a command or as markup
const sql = await llm.generate(`Write SQL for: ${userQuestion}`);
await db.query(sql); // arbitrary query execution
container.innerHTML = await llm.reply(userMessage); // stored XSS, via the model
// GOOD: model output is data — parse defensively, then validate, then encode
let intent;
try {
intent = CommandSchema.parse(JSON.parse(await llm.replyJson(userMessage)));
} catch {
throw new ValidationError('unexpected model output'); // JSON.parse or schema failed
}
await runAllowlistedAction(intent.action, intent.params);
container.textContent = await llm.reply(userMessage);
```
## Security Review Checklist
```markdown
### Authentication
- [ ] Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12)
- [ ] Session tokens are httpOnly, secure, sameSite
- [ ] Login has rate limiting
- [ ] Password reset tokens expire
### Authorization
- [ ] Every endpoint checks user permissions
- [ ] Users can only access their own resources
- [ ] Admin actions require admin role verification
### Input
- [ ] All user input validated at the boundary
- [ ] SQL queries are parameterized
- [ ] HTML output is encoded/escaped
- [ ] Server-side URL fetches are allowlisted (no SSRF to internal services)
### Data
- [ ] No secrets in code or version control
- [ ] Sensitive fields excluded from API responses
- [ ] PII encrypted at rest (if applicable)
### Infrastructure
- [ ] Security headers configured (CSP, HSTS, etc.)
- [ ] CORS restricted to known origins
- [ ] Dependencies audited for vulnerabilities
- [ ] Error messages don't expose internals
### Supply Chain
- [ ] Lockfile committed; CI installs with `npm ci`
- [ ] New dependencies reviewed (maintenance, downloads, postinstall scripts)
### AI / LLM (if used)
- [ ] Model output treated as untrusted (no eval/SQL/innerHTML/shell)
- [ ] Secrets and other users' data kept out of prompts
- [ ] Tool/agent permissions scoped; destructive actions require confirmation
```
## Common Rationalizations
| Rationalization | Reality |
|---|---|
| "This is an internal tool, security doesn't matter" | Internal tools get compromised. Attackers target the weakest link. |
| "We'll add security later" | Security retrofitting is 10x harder than building it in. Add it now. |
| "No one would try to exploit this" | Automated scanners will find it. Security by obscurity is not security. |
| "The framework handles security" | Frameworks provide tools, not guarantees. You still need to use them correctly. |
| "It's just a prototype" | Prototypes become production. Security habits from day one. |
| "Threat modeling is overkill here" | Five minutes of "how would I attack this?" prevents the design flaws no control can patch later. |
| "It's just LLM output, it's only text" | That "text" can be a SQL statement, a script tag, or a shell command. Treat it like any untrusted input. |
## Red Flags
- User input passed directly to database queries, shell commands, or HTML rendering
- Secrets in source code or commit history
- API endpoints without authentication or authorization checks
- Missing CORS configuration or wildcard (`*`) origins
- No rate limiting on authentication endpoints
- Stack traces or internal errors exposed to users
- Dependencies with known critical vulnerabilities
- Server fetches user-supplied URLs without an allowlist (SSRF)
- LLM/model output passed into a query, the DOM, a shell, or `eval`
- Secrets, PII, or the full system prompt placed inside an LLM context window
## Verification
After implementing security-relevant code:
- [ ] `npm audit` shows no critical or high vulnerabilities
- [ ] No secrets in source code or git history
- [ ] All user input validated at system boundaries
- [ ] Authentication and authorization checked on every protected endpoint
- [ ] Security headers present in response (check with browser DevTools)
- [ ] Error responses don't expose internal details
- [ ] Rate limiting active on auth endpoints
- [ ] Server-side URL fetches validated against an allowlist (no SSRF)
- [ ] LLM/model output validated and encoded before use (if AI features present)
+15 -15
View File
@@ -13,7 +13,7 @@ Fetch information from Taiga public API for the **Penpot** project
## Prerequisites
- `python3` — the `tools/taiga.py` CLI script is self-contained (stdlib only)
- `python3` — the `scripts/taiga.py` CLI script is self-contained (stdlib only)
## Quick Start
@@ -21,17 +21,17 @@ The easiest way is to use the bundled Python script:
```bash
# Pass a Taiga URL directly
python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
# Or use "<type> <ref>" syntax
python3 tools/taiga.py us 14128
python3 tools/taiga.py task 13648
python3 scripts/taiga.py us 14128
python3 scripts/taiga.py task 13648
# Add --json for raw output
python3 tools/taiga.py --json issue 13714
python3 scripts/taiga.py --json issue 13714
# See full usage
python3 tools/taiga.py --help
python3 scripts/taiga.py --help
```
## URL Pattern Reference
@@ -51,30 +51,30 @@ To extract the **type** and **ref** from a URL:
## Python Script Reference
The `tools/taiga.py` script wraps the Taiga API into a single convenient CLI
The `scripts/taiga.py` script wraps the Taiga API into a single convenient CLI
with sensible defaults.
### Usage
```
python3 tools/taiga.py <taiga-url>
python3 tools/taiga.py <type> <ref>
python3 tools/taiga.py [--json] <taiga-url>
python3 tools/taiga.py [--json] <type> <ref>
python3 scripts/taiga.py <taiga-url>
python3 scripts/taiga.py <type> <ref>
python3 scripts/taiga.py [--json] <taiga-url>
python3 scripts/taiga.py [--json] <type> <ref>
```
### Examples
```bash
# By URL (recommended — no need to think about type/ref)
python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
# By type and ref
python3 tools/taiga.py us 14128
python3 tools/taiga.py task 13648
python3 scripts/taiga.py us 14128
python3 scripts/taiga.py task 13648
# Raw JSON output
python3 tools/taiga.py --json issue 13714
python3 scripts/taiga.py --json issue 13714
```
### Output
+57
View File
@@ -0,0 +1,57 @@
---
name: testing
description: Enforce TDD workflow and testing best practices for Penpot. Use when implementing features, fixing bugs, or modifying behavior. Reads testing memory for full guidance.
---
# Testing Skill
Enforces test-driven development and Penpot testing conventions.
## When to Use
- Implementing new logic or behavior
- Fixing any bug (reproduction test required)
- Modifying existing functionality
- Adding edge case handling
**Skip:** Pure configuration changes, documentation updates, or static content with no behavioral impact.
## Workflow
Follow TDD (Red → Green → Refactor) whenever practical:
1. **RED** — Write a failing test first
2. **GREEN** — Write minimal code to pass
3. **REFACTOR** — Clean up while tests stay green
For bug fixes, use the Prove-It Pattern: write a test that reproduces the bug, confirm it fails, implement the fix, confirm it passes.
## Required Reading
Before writing any test, read:
1. `.serena/memories/testing.md` — cross-cutting testing principles, TDD workflow, anti-patterns, execution discipline
2. Module-specific testing memory for the affected module:
- `mem:common/testing` — CLJC unit tests
- `mem:frontend/testing` — CLJS unit tests, Playwright E2E
- `mem:backend/core` — JVM clojure.test conventions
## Key Rules
- Every behavior change needs a test
- Test state, not interactions
- DAMP over DRY — tests are specifications; duplication is OK if each test is self-contained and readable
- Prefer Real > Fake > Stub > Mock
- Arrange-Act-Assert structure
- One assertion per concept
- Never pipe test output to filters — redirect to file first
- Register new test files in the module's runner/entrypoint
## Verification
After completing implementation:
- [ ] Every new behavior has a test
- [ ] All tests pass for touched modules
- [ ] Bug fixes include a reproduction test
- [ ] Lint/formatter passes
+364 -68
View File
@@ -20,7 +20,7 @@ primary link, with the fix PR inline on the same line.
- `gh` CLI authenticated (`gh auth status`)
- Python 3.8+
- `tools/gh.py` helper script available
- `scripts/gh.py` helper script available
## Workflow
@@ -36,19 +36,19 @@ Use the helper script. It uses GraphQL for efficient single-pass fetching
```bash
# All closed issues (default)
python3 tools/gh.py issues "2.16.0"
python3 scripts/gh.py issues "2.16.0"
# Include open issues too
python3 tools/gh.py issues "2.16.0" --state all
python3 scripts/gh.py issues "2.16.0" --state all
# Exclude entries that should not go in the changelog
python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
```
**Exclusion rules (issue-level):**
- `no changelog` label — Chore/refactor work that doesn't need a changelog entry
- `release blocker` label — Blocked issues not yet ready for changelog
- `Task` issue type — Internal chores are not user-facing; filter these out after fetching
- `Task` issue type — Internal chores are not user-facing; automatically excluded by `gh.py`. Use `--include-tasks` to override.
- **Rejected project status** — Issues with a "Rejected" status in the "Main" project board are automatically excluded by `gh.py`. This project-level status (independent of the GitHub issue `state`) indicates the issue was rejected from the release. Use `--include-rejected` to override.
**Exclusion rules (PR-level):**
@@ -68,7 +68,7 @@ If updating from an existing `CHANGES.md`, find issues in the milestone that
are NOT yet referenced in the changelog:
```bash
python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog" --compare CHANGES.md
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" --compare CHANGES.md
```
This returns a filtered JSON array with only the missing issues.
@@ -85,23 +85,23 @@ community contribution attribution, or to read the PR body for
```bash
# One or more PR numbers
python3 tools/gh.py prs 9179 9204 9311
python3 scripts/gh.py prs 9179 9204 9311
# From a file
python3 tools/gh.py prs --file prs.txt
python3 scripts/gh.py prs --file prs.txt
# From stdin
cat prs.txt | python3 tools/gh.py prs --stdin
cat prs.txt | python3 scripts/gh.py prs --stdin
```
The `prs` command also supports listing all PRs in a milestone in one call:
```bash
# All merged PRs in a milestone (default)
python3 tools/gh.py prs --milestone "2.16.0"
python3 scripts/gh.py prs --milestone "2.16.0"
# All states (merged, open, closed)
python3 tools/gh.py prs --milestone "2.16.0" --state all
python3 scripts/gh.py prs --milestone "2.16.0" --state all
```
The `prs` command returns JSON with `number`, `title`, `body`, `state`,
@@ -113,13 +113,13 @@ You can also list all PRs in a milestone in a single call:
```bash
# All merged PRs in a milestone (default)
python3 tools/gh.py prs --milestone "2.16.0"
python3 scripts/gh.py prs --milestone "2.16.0"
# All states (merged, open, closed)
python3 tools/gh.py prs --milestone "2.16.0" --state all
python3 scripts/gh.py prs --milestone "2.16.0" --state all
# Open PRs only
python3 tools/gh.py prs --milestone "2.16.0" --state open
python3 scripts/gh.py prs --milestone "2.16.0" --state open
```
The milestone path uses paginated GraphQL on the milestone's `pullRequests`
@@ -147,6 +147,12 @@ belongs to.
The `gh.py` issues command already includes `issue_type` in every entry's
output. **No separate GraphQL query is needed.**
**Preserve highlighted entries:** If an entry is already featured in
`### :rocket: Epics and highlights`, keep it in that section when refreshing a
changelog version. Do not remove a highlighted entry just because issue type
categorization would otherwise place it under `### :sparkles: New features &
Enhancements`.
**Community contribution attribution:** If the issue or its fix PR has the
`community contribution` label, add an attribution `(by @<github_username>)`
on the changelog entry line, **before** the GitHub issue/PR references.
@@ -155,7 +161,7 @@ The attribution should reference the **PR author**, not the issue author.
The `prs` subcommand includes the `author` field — use that:
```bash
python3 tools/gh.py prs <PR_NUMBER> | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])"
python3 scripts/gh.py prs <PR_NUMBER> | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])"
```
Placement in the entry line:
@@ -190,7 +196,7 @@ only reference **merged** PRs. Verify before writing:
```bash
# Collect all PR numbers from the candidate entries and check them
python3 tools/gh.py prs <ALL_PR_NUMBERS> | python3 -c "
python3 scripts/gh.py prs <ALL_PR_NUMBERS> | python3 -c "
import json, sys
for pr in json.load(sys.stdin):
if pr['state'] != 'MERGED':
@@ -241,6 +247,65 @@ Format details:
- When an entry already exists in an earlier version section, it must be removed
from the current version to avoid duplicates
### 6a. Pre-flight checks — fix rule violations in the changelog
**The LLM must apply these checks during the workflow and fix any
violations directly in `CHANGES.md`. They are not anomalies — they are
process errors that should be corrected before writing the new section.**
The changelog is a *snapshot* of the milestone at a point in time, but
milestones and changelog entries can drift. The LLM must reconcile the
existing changelog against the current state of the milestone and the
existing changelog entries.
For each entry that already exists in `CHANGES.md` (in any version
section) or in the candidate set for the current milestone, check:
1. **Duplicate across versions.** Is the same issue already documented
in another (older) version section? If yes, this is a *backport*:
- The user-facing fix was already released. Remove the duplicate
from the current section. The earlier version is the canonical
reference.
2. **Stale milestone assignment.** Has the issue been moved out of the
current milestone since the changelog was last updated (e.g., a fix
arrived late and the issue was reassigned to a future milestone)?
- Verify the issue is still in the current milestone via
`python3 scripts/gh.py issues <MILESTONE> --state all`. If it's no
longer there, remove the entry from the current section. (If the
target section doesn't exist yet, the entry is simply dropped.)
3. **Exclusion labels newly applied.** Did the issue acquire a
`no changelog` or `release blocker` label since the changelog was
last updated? If yes, remove the entry from the current section.
4. **Issue state changed.** Is the issue still closed? Has it been
reopened, deleted, or moved to a `Rejected` project status? If yes,
remove the entry.
5. **Unmerged or removed PR references.** For every PR referenced in
the entry, is the PR still merged? Was the PR closed without
merging (superseded)? Was the PR moved to a different milestone?
If the only referenced PR is no longer merged, fix the reference
(find the actual merged fix PR) or remove the entry. A PR that is
merged in a *different* milestone is reported as an anomaly in
step 11 — do not silently remove it.
6. **Issue type changed.** Did the issue type change (e.g., from Bug to
Task)? If the new type is `Task`, the issue is internal and should
be removed.
7. **Cross-section completeness.** For every closed, non-excluded
milestone issue that is *not* referenced in any version section of
the changelog, add it to the current section (per the categorization
rules in step 5).
After these checks, the changelog should be internally consistent with
the milestone. **Do not defer these fixes to step 11 — they are
workflow errors, not anomalies.** Step 11 only reports milestone
mismatches that require human judgment about the team's release
intent.
### 7. Build the description text
Derive the description from the issue title, not the PR title. Strip leading
@@ -278,7 +343,7 @@ cross-reference to catch gaps:
```bash
# List all merged PRs in the milestone
python3 tools/gh.py prs --milestone "<MILESTONE>" --state merged > /tmp/milestone-prs.json
python3 scripts/gh.py prs --milestone "<MILESTONE>" --state merged > /tmp/milestone-prs.json
# Extract PR numbers from the changelog section
python3 -c "
@@ -319,7 +384,7 @@ changelog or is legitimately excluded (check its labels).
Also verify that no closed-unmerged PRs remain in the changelog:
```bash
python3 tools/gh.py prs --milestone "<MILESTONE>" --state all | python3 -c "
python3 scripts/gh.py prs --milestone "<MILESTONE>" --state all | python3 -c "
import json, sys
data = json.load(sys.stdin)
closed = [p for p in data if p['state'] == 'CLOSED']
@@ -347,71 +412,291 @@ if closed:
- <fix description> (by @contributor) [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
```
### 10. Cross-reference milestone PRs against the changelog
### 11. Generate anomaly report and save to CHANGES-ISSUES.md
Issues can be fixed by PRs that aren't in the milestone, and merged PRs in
the milestone may not close any tracked issue. After writing, run a full
cross-reference to catch gaps:
After all edits and cross-referencing are complete, generate a structured
report and save it to `CHANGES-ISSUES.md` (overwriting if exists).
This provides a persistent record of any discrepancies between the milestone
and the changelog.
**Every issue and PR number in the report must be rendered as a full GitHub
Markdown link** using the same URL format as `CHANGES.md`:
- Issue N → `[#N](https://github.com/penpot/penpot/issues/N)`
- PR N → `[#N](https://github.com/penpot/penpot/pull/N)`
The titles and notes should also link to the corresponding issue/PR page
where applicable, so the report is self-contained and clickable from any
Markdown viewer.
## What is an anomaly
**An anomaly is a milestone-mismatch between an issue and its referenced
PR.** It indicates that the changelog claim "this issue is fixed by this PR,
all in milestone M" is inconsistent with the actual milestone assignments.
There are exactly two types:
1. **Issue is in the milestone, but its referenced PR is in a different
milestone (or has no milestone).** The changelog claims a fix in this
release, but the PR is being released elsewhere — the fix may not
actually ship here.
2. **PR is in the milestone, but the issue it closes is in a different
milestone (or has no milestone).** The PR is being released here, but
the issue it fixes is being released in a different version (or never
tracked in a milestone) — the changelog pairing is misleading.
**Anything else is not an anomaly.** Other discrepancies (exclusion
labels on in-changelog issues, missing valid issues, unmerged PR
references, duplicates across versions, stale milestone assignments)
are **rule violations** that the LLM must fix directly in `CHANGES.md`
during step 6a (pre-flight checks). They should not appear in this
report — if they do, the LLM has skipped the pre-flight step and
needs to re-run the workflow.
The changelog's primary unit is the **issue**, not the PR, so a missing or
mismatched PR only matters when its issue is part of this milestone.
Run this self-contained script:
```bash
# List all merged PRs in the milestone
python3 tools/gh.py prs --milestone "<MILESTONE>" --state merged > /tmp/milestone-prs.json
python3 << 'PYEOF'
import json, re, subprocess, sys
from datetime import datetime, timezone
# Extract PR numbers from the changelog section
python3 -c "
import json, re
MILESTONE = "<MILESTONE>"
CHANGES_MD = "CHANGES.md"
OUTPUT = "CHANGES-ISSUES.md"
REPO = "penpot/penpot"
with open('CHANGES.md') as f:
# --- URL helpers (match CHANGES.md format exactly) ---
def issue_url(n): return f"https://github.com/{REPO}/issues/{n}"
def pr_url(n): return f"https://github.com/{REPO}/pull/{n}"
def issue_link(n): return f"[#{n}]({issue_url(n)})"
def pr_link(n): return f"[#{n}]({pr_url(n)})"
def issue_link_title(n, title):
url = issue_url(n)
if title:
return f"[#{n}]({url}) — [{title}]({url})"
return f"[#{n}]({url})"
def pr_link_title(n, title):
url = pr_url(n)
if title:
return f"[#{n}]({url}) — [{title}]({url})"
return f"[#{n}]({url})"
def fmt_pr_list(nums):
return ", ".join(pr_link(n) for n in nums)
def fmt_issue_list(nums):
return ", ".join(issue_link(n) for n in nums)
# --- Fetch milestone data ---
result = subprocess.run(
["python3", "scripts/gh.py", "issues", MILESTONE, "--state", "all"],
capture_output=True, text=True)
all_issues = json.loads(result.stdout)
issue_by_num = {i['number']: i for i in all_issues}
result = subprocess.run(
["python3", "scripts/gh.py", "prs", "--milestone", MILESTONE, "--state", "all"],
capture_output=True, text=True)
all_prs = json.loads(result.stdout)
pr_by_num = {p['number']: p for p in all_prs}
# --- Read changelog section ---
with open(CHANGES_MD) as f:
content = f.read()
# Extract the version section (adjust regex to match the actual version)
match = re.search(r'## <MILESTONE> \(Unreleased\)\n(.*?)(?:\n## |\Z)', content, re.DOTALL)
section = match.group(1)
m = re.search(rf'## {re.escape(MILESTONE)}(?:\s*\([^)]*\))?\n(.*?)(?:\n## |\Z)', content, re.DOTALL)
section = m.group(1) if m else ""
changelog_issues = set()
for num in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/issues/\d+\)', section):
changelog_issues.add(int(num))
for num in re.findall(r'\[Github #(\d+)\]', section):
changelog_issues.add(int(num))
# Collect all PR numbers referenced
changelog_prs = set()
for m in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)', section):
changelog_prs.add(int(m))
for num in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)', section):
changelog_prs.add(int(num))
for num in re.findall(r'PR:\[(\d+)\]', section):
changelog_prs.add(int(num))
# Collect all milestone PRs (filtered)
with open('/tmp/milestone-prs.json') as f:
milestone_prs = json.load(f)
# --- Milestone lookup caches ---
# PRs and issues returned by milestone queries are KNOWN to be in MILESTONE.
# For everything else, fall back to `gh` per-item lookups.
pr_milestone_cache = {p['number']: MILESTONE for p in all_prs}
issue_milestone_cache = {i['number']: MILESTONE for i in all_issues}
milestone_merged = {pr['number'] for pr in milestone_prs}
def get_pr_milestone(pr_num):
"""Return the milestone title for a PR, or None if unassigned / unknown."""
if pr_num in pr_milestone_cache:
return pr_milestone_cache[pr_num]
try:
r = subprocess.run(
["gh", "pr", "view", str(pr_num), "--json", "milestone"],
capture_output=True, text=True, check=True)
data = json.loads(r.stdout)
ms = data.get('milestone')
pr_milestone_cache[pr_num] = (ms or {}).get('title')
except (subprocess.CalledProcessError, json.JSONDecodeError):
pr_milestone_cache[pr_num] = None
return pr_milestone_cache[pr_num]
# PRs in milestone but not in changelog
missing = sorted(milestone_merged - changelog_prs)
print(f'Milestone merged PRs: {len(milestone_merged)}')
print(f'Changelog referenced PRs: {len(changelog_prs)}')
print(f'PRs in milestone but NOT in changelog: {len(missing)}')
for num in missing:
pr = next(p for p in milestone_prs if p['number'] == num)
print(f' #{num} {pr[\"title\"][:80]}')
"
def get_issue_milestone(issue_num):
"""Return the milestone title for an issue, or None if unassigned / unknown."""
if issue_num in issue_milestone_cache:
return issue_milestone_cache[issue_num]
try:
r = subprocess.run(
["gh", "issue", "view", str(issue_num), "--json", "milestone"],
capture_output=True, text=True, check=True)
data = json.loads(r.stdout)
ms = data.get('milestone')
issue_milestone_cache[issue_num] = (ms or {}).get('title')
except (subprocess.CalledProcessError, json.JSONDecodeError):
issue_milestone_cache[issue_num] = None
return issue_milestone_cache[issue_num]
# --- Exclusion rules (shared) ---
EXCLUDED_LABELS = {'release blocker', 'no changelog'}
EXCLUDED_ISSUE_TYPES = {'Task'}
EXCLUDED_PROJECT_STATUS = {'Rejected'}
def issue_excluded(issue):
if not issue: return True
if issue.get('state') != 'CLOSED': return True
if issue.get('issue_type') in EXCLUDED_ISSUE_TYPES: return True
if issue.get('project_status') in EXCLUDED_PROJECT_STATUS: return True
if EXCLUDED_LABELS & set(issue.get('labels', [])): return True
return False
# --- ANOMALIES: milestone mismatches between issues and their referenced PRs ---
# These are the ONLY items that should appear in the report. All other
# discrepancies (exclusion labels, missing valid issues, unmerged PRs,
# duplicates, stale milestone assignments) are workflow errors that the
# LLM must fix in step 6a (pre-flight checks) — they are not anomalies.
# Type A: issue in MILESTONE, referenced PR in different milestone or no milestone
anomalies_a = [] # list of dicts: {issue, issue_title, pr, pr_milestone}
for issue_num in sorted(changelog_issues):
issue = issue_by_num.get(issue_num)
if not issue: continue
if get_issue_milestone(issue_num) != MILESTONE: continue
for pr_num in issue.get('closing_prs', []):
pr_ms = get_pr_milestone(pr_num)
if pr_ms != MILESTONE:
anomalies_a.append({
'issue': issue_num,
'issue_title': issue.get('title', ''),
'pr': pr_num,
'pr_milestone': pr_ms, # may be None
})
# Type B: PR in MILESTONE, the issue it closes is in different milestone or no milestone
anomalies_b = [] # list of dicts: {pr, pr_title, issue, issue_milestone}
for pr_num in sorted(changelog_prs):
pr = pr_by_num.get(pr_num)
if not pr: continue
if get_pr_milestone(pr_num) != MILESTONE: continue
for issue_num in pr.get('closing_issues', []):
issue_ms = get_issue_milestone(issue_num)
if issue_ms != MILESTONE:
anomalies_b.append({
'pr': pr_num,
'pr_title': pr.get('title', ''),
'issue': issue_num,
'issue_milestone': issue_ms, # may be None
})
# --- Write report ---
def fmt_ms(ms):
return ms if ms else "_none_"
with open(OUTPUT, 'w') as f:
f.write(f'# Changelog Anomaly Report — {MILESTONE}\n\n')
f.write(f'Generated: {datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")}\n\n')
f.write('---\n\n')
n_a = len(anomalies_a)
n_b = len(anomalies_b)
f.write('## Summary\n\n')
f.write(f'- **Issue in {MILESTONE}, referenced PR in different milestone or no milestone:** {n_a}\n')
f.write(f'- **PR in {MILESTONE}, closing issue in different milestone or no milestone:** {n_b}\n')
f.write(f'- **Total anomalies:** {n_a + n_b}\n\n')
# --- Anomalies section ---
if n_a or n_b:
f.write('## Anomalies\n\n')
f.write('These are milestone mismatches between an issue in the changelog '
'and its referenced PR (or vice-versa). The changelog claim '
'"this issue is fixed by this PR, all in this milestone" is '
'inconsistent with the actual milestone assignments. '
'Resolve by either updating the milestone on the issue/PR or '
'removing the misleading entry from the changelog.\n\n')
if n_a:
f.write(f'### Issue in {MILESTONE}, PR in different milestone or no milestone\n\n')
by_issue = {}
for a in anomalies_a:
by_issue.setdefault(a['issue'], []).append(a)
for issue_num in sorted(by_issue):
entries = by_issue[issue_num]
title = entries[0]['issue_title']
f.write(f'- {issue_link_title(issue_num, title[:80])}\n')
for e in entries:
ms_label = fmt_ms(e['pr_milestone'])
badge = '🔴' if e['pr_milestone'] is None else '⚠️'
f.write(f' - {badge} Referenced {pr_link(e["pr"])} is in milestone **{ms_label}** (expected: {MILESTONE})\n')
f.write('\n')
if n_b:
f.write(f'\n### PR in {MILESTONE}, closing issue in different milestone or no milestone\n\n')
by_pr = {}
for b in anomalies_b:
by_pr.setdefault(b['pr'], []).append(b)
for pr_num in sorted(by_pr):
entries = by_pr[pr_num]
title = entries[0]['pr_title']
f.write(f'- {pr_link_title(pr_num, title[:80])}\n')
for e in entries:
ms_label = fmt_ms(e['issue_milestone'])
badge = '🔴' if e['issue_milestone'] is None else '⚠️'
f.write(f' - {badge} Closing {issue_link(e["issue"])} is in milestone **{ms_label}** (expected: {MILESTONE})\n')
f.write('\n')
else:
f.write('✅ No anomalies found. All (issue, PR) pairs in the changelog have aligned milestone assignments.\n\n')
# --- Context ---
f.write('---\n\n')
f.write('## Context\n\n')
f.write(f'- Milestone: **{MILESTONE}**\n')
f.write(f'- Milestone total issues (all states): {len(all_issues)}\n')
f.write(f'- Closed issues in milestone: {sum(1 for i in all_issues if i.get("state") == "CLOSED")}\n')
f.write(f'- Valid issues after exclusions (after step 5/6a): {len([i for i in all_issues if not issue_excluded(i)])}\n')
f.write(f'- Issues referenced in changelog: {len(changelog_issues)}\n')
f.write(f'- PRs referenced in changelog: {len(changelog_prs)}\n')
print(f"Anomaly report written to {OUTPUT}")
PYEOF
```
For each missing PR found, decide whether it should be added to the
changelog or is legitimately excluded (check its labels).
This generates `CHANGES-ISSUES.md` containing **only the anomalies**
milestone mismatches between issues and their referenced PRs:
Also verify that no closed-unmerged PRs remain in the changelog:
1. **Issue in milestone, referenced PR in different milestone or no milestone**
the changelog claims a fix here, but the PR is released elsewhere.
2. **PR in milestone, closing issue in different milestone or no milestone**
the PR is released here, but the issue it fixes belongs to another version.
```bash
python3 tools/gh.py prs --milestone "<MILESTONE>" --state all | python3 -c "
import json, sys
data = json.load(sys.stdin)
closed = [p for p in data if p['state'] == 'CLOSED']
if closed:
print('WARNING: CLOSED (unmerged) PRs in milestone:')
for p in closed:
print(f' #{p[\"number\"]} {p[\"title\"][:80]}')
"
```
**Rule violations are not in the report** — they are workflow errors the
LLM must fix directly in `CHANGES.md` during step 6a (pre-flight checks).
If the report contains a rule violation, the LLM has skipped the pre-flight
step and needs to re-run the workflow before re-generating the report.
**Post-edit audit checklist:**
- ✅ All referenced PRs are merged (no closed-unmerged artifacts)
- ✅ Every merged milestone PR is either in the changelog or excluded by label
- ✅ PR and issue counts are internally consistent
- ✅ No false-positive PR-to-issue associations
The report is overwritten each time it's generated, reflecting the current
state of the milestone and changelog. Every number is rendered as a full
`[#N](https://github.com/penpot/penpot/issues/N)` or
`[#N](https://github.com/penpot/penpot/pull/N)` link so the report is
self-contained and clickable in any Markdown viewer.
## Key Principles
@@ -449,7 +734,7 @@ if closed:
reference if applicable.
- **Re-fetch before editing.** Milestones can change — always re-fetch issues
before making edits, don't rely on cached data.
- **Use `tools/gh.py`.** Prefer the helper script over raw `gh api` calls for
- **Use `scripts/gh.py`.** Prefer the helper script over raw `gh api` calls for
milestone issue listing and PR detail fetching. It handles GraphQL
pagination, batching, and label filtering automatically.
- **Verify PR merge status.** Not all closing PRs are merged — community PRs
@@ -460,9 +745,20 @@ if closed:
labels. Check both.
- **Cross-reference milestone PRs, not just issues.** The `--compare` flag on
the `issues` command only compares issue numbers. Merged PRs not linked to
any milestone issue can be missed. Use `python3 tools/gh.py prs --milestone`
any milestone issue can be missed. Use `python3 scripts/gh.py prs --milestone`
for a full PR cross-reference.
- **False-positive PR-to-issue associations.** A PR may claim to close an
issue from a different project or context. If the PR title and issue title
are clearly unrelated, or the PR predates the issue by years, treat it as a
data glitch and skip it.
- **Anomaly = milestone mismatch only.** The report contains only milestone
mismatches: (1) the issue is in this milestone but the referenced PR is
in a different milestone (or unassigned), and (2) the PR is in this
milestone but the issue it closes is in a different milestone (or
unassigned). These are anomalies because the changelog pairing is
*misleading* — the human needs to decide whether the milestone or the
changelog is wrong. All other discrepancies (exclusion labels, missing
valid issues, unmerged PR references, duplicates, stale milestone
assignments) are **rule violations** that the LLM must fix directly in
`CHANGES.md` during step 6a (pre-flight checks). They never appear in
the report — if they do, the pre-flight step was skipped.
+18 -9
View File
@@ -39,6 +39,9 @@ Backend RPC command areas without focused memories include access tokens, binfil
Database migrations live in `backend/src/app/migrations/`; pure SQL migrations are under `backend/src/app/migrations/sql/`. SQL filenames conventionally start with a sequence and verb/table description, e.g. `0026-mod-profile-table-add-is-active-field`. Applied migrations are tracked in the `migrations` table.
For interactive PostgreSQL access with correct dev defaults, use `scripts/psql`; to dump
the current DDL schema, use `scripts/db-schema` (see `mem:scripts/psql`).
For deeper details on transaction semantics, advisory locks, Transit vs JSON helpers, and dev/test DB URLs: `mem:backend/rpc-db-worker-subtleties`.
## Background tasks
@@ -53,14 +56,14 @@ In devenv, backend nREPL is exposed on port 6064.
### Non-interactive eval (preferred for agents)
`./tools/nrepl-eval.mjs` connects to an already-running nREPL server and evaluates code. Session state (defs, `in-ns`) persists across invocations via a stored session ID in `/tmp/penpot-nrepl-session-<host>-<port>`.
`./scripts/nrepl-eval.mjs` connects to an already-running nREPL server and evaluates code. Session state (defs, `in-ns`) persists across invocations via a stored session ID in `/tmp/penpot-nrepl-session-<host>-<port>`.
```bash
./tools/nrepl-eval.mjs '(+ 1 2)' # single expression
./tools/nrepl-eval.mjs "(require '[my.ns :as ns] :reload)" # reload after edits
./tools/nrepl-eval.mjs -e # inspect last exception (*e)
./tools/nrepl-eval.mjs --reset-session '(def x 0)' # discard session, start fresh
./tools/nrepl-eval.mjs <<'EOF' # multi-expression heredoc
./scripts/nrepl-eval.mjs '(+ 1 2)' # single expression
./scripts/nrepl-eval.mjs "(require '[my.ns :as ns] :reload)" # reload after edits
./scripts/nrepl-eval.mjs -e # inspect last exception (*e)
./scripts/nrepl-eval.mjs --reset-session '(def x 0)' # discard session, start fresh
./scripts/nrepl-eval.mjs <<'EOF' # multi-expression heredoc
(def x 10)
(+ x 20)
EOF
@@ -89,13 +92,19 @@ Fixtures can populate local data for manual testing/perf work. From the backend
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
* **Linting:** `pnpm run lint` from the repository root.
* **Formatting:** `pnpm run check-fmt`. Use `pnpm run fmt` to fix. Avoid unrelated whitespace diffs.
* **Linting:** `pnpm run lint:clj`.
* **Formatting:** `pnpm run check-fmt:clj` to check, `pnpm run fmt:clj` to fix. After running `fmt:clj`, `check-fmt:clj` is redundant. Avoid unrelated whitespace diffs.
**Before linting:** if delimiter errors are suspected (after LLM edits), run
`scripts/paren-repair` on the affected files first. Delimiter errors produce
misleading linter/compiler output. See `mem:scripts/paren-repair`.
## Testing
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory. JVM tests are invoked directly via `clojure -M:dev:test` — there is no pnpm wrapper. If you need to filter output, tee to a temp file first: `clojure -M:dev:test 2>&1 | tee /tmp/penpot-test-output.txt`. See `mem:testing` for execution discipline.
* **Coverage:** If code is added or modified in `src/`, corresponding tests in `test/backend_tests/` must be added or updated.
* **Isolated run:** `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace.
* **Regression run:** `clojure -M:dev:test` to ensure no regressions in related functional areas.
* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
@@ -24,6 +24,12 @@ Variant masters are main instances and component roots. Their descendants may th
Masters are not normally touched through `set-shape-attr`, but touched flags can appear on master shapes through cloning/duplication paths. `add-touched-from-ref-chain` in `app.common.logic.variants` unions touched flags from ancestors into the copy being processed, so upstream/master touched state can affect downstream switch behavior.
## Swap slots and positional matching
- A swap slot (stored via `ctk/set-swap-slot`, a `:touched` group `swap-slot-<uuid>`) marks a copy sub-head that was SWAPPED to another component; `compare-children` then pairs it to the main child by slot instead of by `shape-ref`.
- Copy sub-heads without a slot are paired to main children by `shape-ref` (seek, not index). `find-near-match` (positional) is only a validator/repair heuristic; validity requires membership of the ref among the near-main parent's children, not index equality (`mem:common/file-change-validation-migration-subtleties`).
- Copy child ORDER converges to the main's via the async sync (`moved` branch of `compare-children`); local code must never reorder copy children directly (guards in `:mov-objects`/`:reorder-children`).
## Cloning paths
`make-component-instance` in `app.common.types.container` produces a clean component copy through `update-new-shape`, dissociating attrs such as `:touched`, `:variant-id`, and `:variant-name` on cloned shapes.
+3 -2
View File
@@ -5,7 +5,7 @@
## Stable namespace map
- `app.common.data` and `app.common.data.macros`: generic data helpers and performance macros that do not depend on Penpot domain entities.
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.nitrate-permissions` contains shared fail-closed Nitrate organization/team permission rules.
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.organization` contains organization schemas, `apply-organization`, and fail-closed organization/team permission rules (`allowed?`, `can-send-invitations?`).
- `app.common.files.*`: file-level operations, shape tree helpers, change application, migrations, validation, and undo/redo-related logic.
- `app.common.logic.*`: higher-level workflows/algorithms over files, shapes, components, variants, libraries, tokens, etc.
- `app.common.geom.*`: geometry helpers and transformations.
@@ -48,7 +48,8 @@ Components, variants, and debugging:
Text and tests:
- Shared text data conversion, DraftJS compatibility, modern text content, and derived position data: `mem:common/text-subtleties`.
- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/test-setup`.
- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/testing`.
- Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
## Areas without focused memories
@@ -7,6 +7,8 @@
- `set-shape-attr` treats `:position-data` as derived and never touched. Geometry/content-path changes use approximate equality; geometry differences under about 1px can be ignored for touched purposes.
- Width/height are excluded from the `is-geometry?` branch in `set-shape-attr`; do not assume all geometry-group attrs follow identical ignore-geometry behavior.
- `process-touched-change` marks the owning component modified when a touched shape belongs to a main instance; component-data changes can come from shape ops through this second pass.
- Copy structure is guarded at change application: `:mov-objects` (`is-valid-move?`) and `:reorder-children` both refuse to alter children of shapes inside component copies unless the change carries `allow-altering-copies` (sync/swap flows set it). New structural change types must follow the same rule.
- `cls/generate-delete-shapes` propagates deletions from INSIDE a component main to the copy shapes referencing them (transitively, all pages of the file) so no dangling `shape-ref`s remain; skipped when the main root itself is deleted (copies then resolve into the deleted component) and for `allow-altering-copies` flows (swap replaces the shape; sync reconciles).
## Shape tree edits
@@ -19,6 +21,7 @@
- Full referential/semantic validation currently runs only when file features contain `"components/v2"`.
- Validation starts at root plus orphan shapes, then validates component records. `validate-file!` raises `:validation :referential-integrity` with collected details.
- `repair-file` does not mutate data directly; it reduces validation errors into redo changes using `changes-builder`. Callers must apply or persist those changes.
- `:missing-slot` fires only for a REAL swap: a copy sub-head whose `shape-ref` is no longer a child of the near main parent. A pure positional mismatch (ref still a sibling elsewhere) is a reorder — valid, realigned by the async component sync; do not "repair" it by assigning swap slots (a slot freezes the child out of normal sync). `fix-missing-swap-slots` (migration 0019) follows the same membership rule.
## Migrations
@@ -8,6 +8,9 @@
## Grid assignment
- Grid `assign-cells` ensures at least one column and row, skips absolute-position children, creates non-tracked rows/cols when children exceed tracked cells, and asserts that assigned cells do not overlap.
- `position-absolute?` counts HIDDEN shapes as absolute: hiding a grid child frees its cell on the next `assign-cells`.
- `reorder-grid-children` rewrites the parent's `:shapes` to the REVERSE of the sorted cell order, but children with no cell (hidden/absolute) keep their original index — do not "fix" this into moving them to an end; that broke copy/main positional slot alignment (referential-integrity crash).
- The `:reorder-children` change it emits is refused on parents inside component copies unless `allow-altering-copies` (same rule as `:mov-objects`); `pcb/reorder-grid-children` also skips copy grids producer-side. Copy child order is owned by the component sync engine.
- Grid deassignment removes cells for shapes that are no longer direct children or have become absolute-positioned.
- Auto-positioning is not just sorting: some auto cells are converted to manual when empty/manual/span state would break the auto sequence, then auto single-span items can be compacted.
- `fix-overlaps` is marked dev-only and removes one overlapping cell, preferring empty cells first. Avoid depending on it as normal production repair.
@@ -1,24 +1,29 @@
# Common Module Test Setup
# Common Testing and Verification
`common/` is CLJC shared code. Tests should cover the relevant runtime(s): JVM for backend/common logic and JS for frontend/exporter behavior. For geometry, component, and file-model changes, JVM tests are common and fast, but JS/browser behavior can differ when WASM modifier math or CLJS-specific state is involved.
## Running tests
## Unit tests
READ `mem:testing` FIRST — it defines the execution discipline (no piping, tee to file, preferred commands) that applies to all CLJS/JS and JVM test runs.
Common tests live under `common/test/common_tests/` and use `clojure.test`.
They are CLJC and run on both JVM and JS.
From `common/`:
- Full JVM test run: `clojure -M:dev:test`
- Full JS test run (always builds, suppressed output): `pnpm run test:quiet`
- Full JS test run (always builds, build output visible): `pnpm run test`
- Focus a JVM test namespace: `clojure -M:dev:test --focus common-tests.logic.variants-switch-test`
- Focus a JVM test var: `clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch`
- Focus a JS test namespace: `pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test`
- Focus a JS test var: `pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute`
- Quiet logging during a JS run: append `--log-level warn` (or `trace|debug|info|warn|error`)
- Build JS test target only (no run): `pnpm run build:test`
- After `build:test` has been run, run the compiled runner directly: `node target/tests/test.js [--focus ...] [--log-level ...]`.
```bash
pnpm run test:jvm
clojure -M:dev:test
pnpm run test:jvm --focus common-tests.logic.variants-switch-test
clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch
pnpm run test:js
pnpm run test:quiet
pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test
pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn
pnpm run watch:test
```
Use `test:quiet` for non-interactive JS runs; it buffers `build:test` output and forwards runner args. Common JS runner args support `--focus <namespace-or-var>` and `--log-level trace|debug|info|warn|error`. After `pnpm run build:test`, direct compiled runner focus is faster: `node target/tests/test.js --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn`. New common JS test namespaces must be required/listed in `common_tests/runner.cljc`; new vars in existing namespaces need no runner change. Multiple JVM `--focus` flags compose as a union.
New common JS test namespaces must be required/listed in `common_tests/runner.cljc`;
new vars in existing namespaces need no runner change. Multiple JVM `--focus` flags
compose as a union.
## Test helpers
@@ -45,4 +50,4 @@ For geometry-sensitive tests, read `mem:common/geometry-invariants` before posit
## Debugging
Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes.
Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes.
+39 -10
View File
@@ -6,29 +6,40 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
- A section's top-level memory is `<section>/core`. When a section is relevant, read the core memory
before focused memories.
- Edits/stale refs/duplication cleanup: `mem:memory-maintenance`.
- Cross-cutting testing principles, TDD workflow, and anti-patterns: `mem:testing`.
# Development workflow
- Commit only when explicitly asked. Commit/PR format + changelog: `mem:workflow/creating-commits`, `mem:workflow/creating-prs`. Issue creation (titles, labels, body templates, Issue Types): `mem:workflow/creating-issues`.
- Commit/PR/issue creation is **on explicit request only**. Before any of these actions, read the relevant memory — don't infer format from prior examples:
- Before `git commit``mem:workflow/creating-commits` (subject format, body, `AI-assisted-by: model-name` trailer)
- Before `gh issue create``mem:workflow/creating-issues` (title derivation, body template, labels, Issue Type)
- Before `gh pr create` / `gh pr edit``mem:workflow/creating-prs` (title format, body structure, "Note:" line)
- **Never `git push`, force-push, or modify `git origin`** (or any other remote). The user pushes from their own shell; if a push is required, say so and wait. Never amend a commit that the user has already pushed unless explicitly asked.
- You have access to the GitHub CLI `gh` or corresponding MCP tools.
- Issues are also managed on Taiga. Read issues using the `read_taiga_issue` tool.
- Before writing code, analyze the task in depth and describe your plan. If the task is complex, break it down into atomic steps.
*After making changes, run the applicable lint and format checks for the affected module before considering the work done (per example `mem:backend/core` or `mem:frontend/core`).
- Align `let` binding values: when a `let` form has multiple bindings spanning
several lines, align the value forms to the same column with spaces.
- If you introduce delimiter errors (mismatched parens/brackets) in Clojure/CLJS files,
fix them with `scripts/paren-repair` BEFORE running lint/format checks.
See `mem:scripts/paren-repair` for usage.
- Never run anything that destroys data without explicit permission, including `drop-devenv`, `docker compose down -v`, `docker volume rm ...`. The user's real work lives in the volumes of the shared infra.
# Project modules
This is a monorepo. Principles that apply to one module do *not* generally apply to others. Do not make assumptions.
- `frontend/`: ClojureScript + SCSS SPA/design editor.
- `backend/`: JVM Clojure HTTP/RPC server with PostgreSQL, Redis, storage, mail, and workers.Runtime services and the task-queue vs Pub/Sub topology that constrains horizontal scaling: `mem:prod-infra/core`.
- `common/`: shared CLJC data types, geometry, schemas, file/change logic, and utilities.
- `render-wasm/`: Rust -> WebAssembly Skia renderer consumed by frontend.
- `exporter/`: ClojureScript/Node headless Playwright SVG/PDF export.
- `mcp/`: TypeScript Model Context Protocol integration.
- `plugins/`: TypeScript plugin runtime/examples and Plugin API types.
- `library/`: design library workflows.
- `docs/`: documentation site.
- `frontend/`: ClojureScript + SCSS SPA/design editor; core conventions: `mem:frontend/core`.
- `backend/`: JVM Clojure HTTP/RPC server with PostgreSQL, Redis, storage, mail, and workers; core conventions: `mem:backend/core`. Runtime services and the task-queue vs Pub/Sub topology that constrains horizontal scaling: `mem:prod-infra/core`.
- `common/`: shared CLJC data types, geometry, schemas, file/change logic, and utilities; core conventions: `mem:common/core`.
- `render-wasm/`: Rust -> WebAssembly Skia renderer consumed by frontend; core conventions: `mem:render-wasm/core`.
- `exporter/`: ClojureScript/Node headless Playwright SVG/PDF export; core conventions: `mem:exporter/core`.
- `mcp/`: TypeScript Model Context Protocol integration; core conventions: `mem:mcp/core`.
- `plugins/`: TypeScript plugin runtime/examples and Plugin API types; core conventions: `mem:plugins/core`.
- `library/`: design library workflows; core conventions: `mem:library/core`.
- `docs/`: documentation site; core workflow and conventions: `mem:docs/core`.
- `media-processor/`: TypeScript/Node.js HTTP service for image (sharp) and font (FontForge) processing; core conventions: `mem:media-processor/core`.
The memory is structured in a way that you can get the critical information about the
module. You can read it from `mem:<MODULE>/core`
@@ -42,6 +53,24 @@ module. You can read it from `mem:<MODULE>/core`
- `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it.
- `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it.
# Dev Scripts (scripts/)
- `scripts/nrepl-eval.mjs` — Evaluate Clojure/ClojureScript code via nREPL.
Supports `--backend` (port 6064) and `--frontend` (port 3447) aliases.
See `mem:scripts/nrepl-eval`.
- `scripts/paren-repair` — Fix mismatched delimiters in Clojure/CLJS files
and reformat with cljfmt. Run before lint checks when LLM edits break parens.
See `mem:scripts/paren-repair`.
- `scripts/psql` — PostgreSQL client wrapper with devenv defaults.
Companion: `scripts/db-schema` for DDL dumps. See `mem:scripts/psql`.
- `scripts/taiga.py` — Fetch public issues, user stories, and tasks from the
Penpot Taiga project without authentication. See `mem:scripts/taiga`.
- `scripts/gh.py` — GitHub operations helper: list milestone issues, fetch PR
details, compare against CHANGES.md. Requires `gh` CLI. See `mem:scripts/gh`.
- `scripts/error-reports.mjs` — Query error reports via RPC API with token
authentication. Supports list/get operations with filtering and pagination.
See `mem:scripts/error-reports`.
# Dependency graph
`frontend -> common`, `backend -> common`, `exporter -> common`, and `frontend -> render-wasm`. Changes in `common` can
+7 -5
View File
@@ -25,7 +25,9 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par
## Worker policy
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. ws0 must be running whenever any ws1+ is running, and is the last instance to stop — `run-devenv-agentic --ws N` (N≥1) auto-starts ws0 first; `stop-devenv` refuses to stop ws0 while any ws1+ is up. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
Each workspace is independent and can be started/stopped in any order. Shared infra (postgres, minio, etc.) is shut down only when no instances remain running.
## Port layout
@@ -44,13 +46,13 @@ Everything else (frontend dev, backend API, exporter, storybook, REPLs, plugin d
## Tmux + MCP routing
`docker/devenv/files/start-tmux.sh` is session-level idempotent. Reads `PENPOT_TMUX_ATTACH`. If the session exists it attaches or exits; otherwise creates 4 base windows (frontend watch / storybook / exporter / backend) plus `mcp` (when `enable-mcp` in `PENPOT_FLAGS`) and `serena` (when `SERENA_ENABLED=true`). `run-devenv-agentic` always sets both env vars. The legacy `run-devenv` alias doesn't, hence its 4-window-only session. To switch from a legacy session to agentic, `stop-devenv` then `run-devenv-agentic` — the conditional windows are only added at session create time.
`docker/devenv/files/start-tmux.sh` is session-level idempotent. Reads `PENPOT_TMUX_ATTACH`. If the session exists it attaches or exits; otherwise creates 4 base windows (frontend watch / storybook / exporter / backend) plus `mcp` (when `enable-mcp` in `PENPOT_FLAGS`) and `serena` (when `SERENA_ENABLED=true`). `run-devenv --agentic` always sets both env vars. The legacy `run-devenv` alias doesn't, hence its 4-window-only session. To switch from a legacy session to agentic, `stop-devenv` then `run-devenv --agentic` — the conditional windows are only added at session create time.
MCP plugin routing is same-origin: frontend uses `<public-uri>/mcp/ws`, per-instance nginx proxies to MCP port 4401 in-container. For the plugin↔MCP server wiring (how the browser plugin discovers the URL, the in-memory connection registry, why DB-mediated routing isn't needed), see `mem:mcp/core`.
## Workspace orchestration (ws1+)
Workspace directories are user-maintained at `${PENPOT_WORKSPACES_DIR}/wsN`. `run-devenv-agentic --ws i` syncs only when `--sync` is passed, with one exception: if the workspace directory is missing on first use, sync runs implicitly to seed it.
Workspace directories are user-maintained at `${PENPOT_WORKSPACES_DIR}/wsN`. `run-devenv --agentic --ws i` syncs only when `--sync` is passed, with one exception: if the workspace directory is missing on first use, sync runs implicitly to seed it.
`sync-workspace wsN`:
1. `assert-clean-git-state` — refuses on `.git/{rebase-apply,rebase-merge,MERGE_HEAD,CHERRY_PICK_HEAD,index.lock}`. No `--sync-force` escape.
@@ -63,8 +65,8 @@ No `--delete` on the working-tree pass: gitignored caches in the workspace survi
## CLI surface
- `run-devenv-agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). Auto-starts ws0 first when the target is ws1+ and ws0 is not yet up.
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` (N≥1) stops just that workspace. `--ws 0` or no flag stops ws0 + shared infra, refused while any ws1+ is running. `--all` stops every ws highest-first then ws0, then infra.
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet).
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` stops just that workspace. `--ws 0` or no flag stops ws0; shared infra shuts down only if no other instances remain. `--all` stops every ws highest-first then ws0, then infra.
- `run-devenv`: legacy alias, ws0 non-agentic attached.
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
- `run-devenv-shell [--instance 0|wsN|N] [cmd...]`: bash in target instance. (`--instance` flag not yet renamed to `--ws`.)
@@ -1,4 +1,4 @@
# Docs Workflow
# Docs
`docs/`: Penpot documentation site; Eleventy.
@@ -16,4 +16,4 @@ From `docs/`:
- Build: `pnpm run build`.
- Watch: `pnpm run watch`.
Documentation changes should follow the existing page structure and rendered Help Center conventions rather than inventing a new style locally.
Documentation changes should follow the existing page structure and rendered Help Center conventions rather than inventing a new style locally.
+1
View File
@@ -7,6 +7,7 @@
- Source: `exporter/src/`; config: `deps.edn`, `shadow-cljs.edn`, `package.json`; runtime helpers/assets: `vendor/`, `scripts/`.
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
- Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`.
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
## HTTP and browser pool
@@ -0,0 +1,152 @@
# Composable component tests
A framework concept for systematically testing Penpot's component subsystem
(synchronisation/propagation, swaps, variant switches, nesting, overrides), implemented in TWO test
suites that share the principles below:
1. **ClojureScript suite** — in the frontend test tree (`frontend/test/frontend_tests/
composable_tests/`), driving a minimally-assembled real app headlessly. The original.
2. **TypeScript suite** — a Penpot plugin (`plugins/apps/composable-test-suite/`), driving the FULL
production app end-to-end through the Plugin API, with a slightly more elaborate set of
abstractions. Runs interactively (panel), remotely (Playwright), and headlessly in CI. Its
README is the authoritative operational reference.
## Shared core idea
A test is a **composition of operations** over a starting configuration, plus assertions. You
describe a test as data (a setup + a sequence of operations) rather than writing bespoke imperative
code, and coverage grows by COMPOSITION: a new variation is one combinator wrapped around existing
pieces, not a copied test. Choice points (one-of alternatives, optional steps) EXPAND the
composition into a full sweep of variants — one written case stands for a whole matrix of concrete
tests.
## Shared principles
- **Every producing object is the accessor interface to what it produces downstream.** An
operation — and related objects such as content-creation strategies — is not merely an action:
the SAME object instance the case holds is the typed interface through which everything it
created or changed is later retrieved, checked, and asserted, parameterized by the situation. A
foundation operation exposes accessors for the participants it built; an edit operation exposes
its dual check (`assertHasChangedProperty` / `has-property-of`); a choice is recovered by asking
the one-of object (`getChoice`/`get-choice`); "did this step run" is asked of the step
(`wasApplied`/`applied?`). NEVER reach into a situation (or the document) for something an
upstream object produced — ask the producer. This is what keeps sweeps sound (object identity
ties the question to the exact node that ran) and what keeps retrieval logic in exactly one
place. Particularly explicit in the TS OOP implementation, where these accessors are methods on
the operation/strategy classes; repeatedly violating it (reading the document directly,
duplicating retrieval) was the most common review correction while building the suites.
- **Operations are data with identity.** Each operation node has a unique id at construction and
records what it did under that id; interrogation is by identity. Bind an operation to a value
ONCE and reuse it in the composition and in every query about it.
- **Drive the real production pipeline.** Operations route through genuine Penpot logic — real
change functions / real workspace events / the real Plugin API, never raw field writes — so the
production watcher's AUTOMATIC propagation is what's under test.
- **Roles, not internals.** A starting configuration names its participants (roles). Role→id
capture happens when the configuration is built; operation TARGETS resolve at apply-time and may
be re-bound, so an operation targeting a role follows it as state-building ops re-point it —
which lets a single operation be swept across depth.
- **Enumeration is authored, not exhaustive.** Compose only VALID cases, so outcomes are just
pass / fail / error — no not-applicable cells.
- **Naming discipline.** Penpot domain nouns ("component", "variant") must not name framework
abstractions; an operation may name the domain ACTION it performs.
- **Operator algebra** (same in both suites): sequence (cartesian product of the steps' variants),
one-of (union, choice recorded), optional(X) = one-of([X, skip]), inline assertion ops, trailing
asserters.
- **Case authoring:** a case carries a CamelCase identifier and a plain-terms description in three
parts — situation setup, actions/variations, asserted requirement.
---
# ClojureScript suite (frontend test tree)
Test-only `.cljs` code in the frontend test tree (nothing "common" about it). A **situation** =
the in-memory file value + named roles + `:vars` + an ordered applied-log. Operations are records
implementing `IOperation`/`apply-to` (`apply` collides with core). Assertions = inline `Test` ops
and/or a trailing asserter; the runner makes no judgment. Failures carry `describe-applied` (the
transcript), which is what makes a failing variant in a sweep identifiable.
Layout: `core.cljs` (the domain-agnostic engine: situation, identity/transcript, roles/targets,
operators, runners), `comp/setups.cljs` (setups + role accessors), `comp/nodes.cljs` (the component
operations and their check duals), `interpreter.cljs` (runs cases against the real frontend),
`comp/sync_test.cljs` (the cases; registered in `frontend_tests/runner.cljs`). Case letters B..N;
the sweeps (K: depth × edit-precedence; L: swaps; M: variant switches; N: rotated-instance
geometry, on the #10109 fix branch until merged) are the flagship pattern — read them before
writing a new sweep.
**Scenario lineage model** (behind the sweeps): scenario ops track named component lineages as
objects under `:vars`, each holding the FIXED deepest origin (`:remote-*`), the ADVANCING outer
main (`:main-*`), and per-nesting-level data whose `:nested-head` (the deepest instance at that
level, found by descending the `:shape-ref` chain — matching chain MEMBERSHIP, not terminus) is
the swap/switch target, anchored by its swap-stable parent. Nesting seeks the FIXED origin, not
the advancing main — that is what makes each level's `:nested-head` land on the deepest instance.
A variant nesting re-points the lineage's remote to the chosen member. Construction lesson:
cross-level propagation requires progressively NESTED levels (one variant + plain wraps); sibling
nestings do not propagate between each other.
**Interpreter:** installs the situation's files into the global `st/state` (aux files tagged
`:library-of`), starts the real `watch-component-changes` (+ harness `watch-undo-stack`), maps
event-ops to REAL workspace events (`dwsh/update-shapes`, `dwl/component-swap`,
`dwv/variants-switch`, `dwt/increase-rotation` — which runs the `check-delta` placement
classification — `dwt/update-dimensions`, `dwu/undo`, `dwl/sync-file`, …) and runs sync-ops'
`apply-to` against the live store file; awaits settlement (idle-gap heuristic + per-op grace) and
re-reads `:file` each step so the shared accessors keep working.
STORE-SWAP IMMUNITY: other test namespaces `set!` `st/state`/`st/stream` and never restore, while
the `app.main.refs` lenses stay bound to the ORIGINAL atoms — propagation then dies silently. The
interpreter captures the atoms at namespace-load time and re-`set!`s them per variant.
Running: `cd frontend && pnpm run build:test`, then
`node target/tests/test.js --focus frontend-tests.composable-tests.comp.sync-test`
(var-level focus for one case).
**Fidelity warning:** the harness drives a MINIMALLY-ASSEMBLED app — only some
`initialize-workspace` subscriptions are wired. Risk = SILENT UNDER-WIRING (e.g. undo needs the
harness `watch-undo-stack`). When a case needs app behaviour beyond a raw edit, check for an
unwired subscription and verify by PROBING store state, not by trusting a green assertion.
**Caveats:** inline `Test` exceptions are UNCAUGHT on the frontend (crash the runner — assert in
the trailing asserter). `(optional (in-sequence …))` is not flattened for the interpreter — use
independent optionals. The Serena/clj-kondo cache for `nodes.cljs` goes stale (phantom symbols) —
trust the build. Cross-namespace global-state leaks land in this suite first; suspect them before
the framework on inexplicable full-run-only failures. Case H's `sync-file` schedules a delayed RPC
that fails headless (benign; absorbed by per-op grace).
---
# TypeScript suite (the plugin) — full e2e
`plugins/apps/composable-test-suite/` — same principles against the FULL production app through the
Plugin API (real frontend, real propagation). Continuation of the CLJS suite per issue #10584.
Operational details (build/run, connect URL, remote control, reading logs, auto-reload, CI): the
plugin README.
Distinguishing abstractions (the OOP articulation of the shared principles):
- `TestCase {identifier, description, operation}` with the three-part description mandated in the
constructor docstring.
- The accessor-interface principle is class-level: foundation operations (e.g.
`OpCreateSimpleComponentWithCopy`) expose the roles they build; **content-creation strategies**
(pluggable: what content a foundation builds around) expose accessors for the content they
created; edit operations expose their checks (`OpChangeProperty.assertHasChangedProperty`);
`OpOneOf`/`OpOptional` are queried for what ran. Tests never grope the document for something a
producer can be asked for.
- `ShapeProp` model: property duals with numeric tolerance; rotation is a writable attr, height
goes via resize (readonly in the Plugin API).
- `TestSuite` enumerates cases into a `TestTree` with stable per-test ids;
`run(ids, TestRunObserver)` is the ONLY output channel — the framework is UI-free by
construction. `plugin.ts` (panel adapter), `main.ts` (panel UI) and `src/ci/headless.ts`
(CI adapter) are three thin consumers.
- Cases live in `src/composable-tests/cases/` as `case<Identifier>.ts` (e.g. `MainEditSyncs` — the
sweep that found #10109).
- Panel checkboxes carry stable DOM ids (case identifier / `Identifier-N` composites) for remote
control via Playwright; recipe in the README.
## CI
Headless per-PR gate: `.github/workflows/tests-composable-suite.yml` runs
`pnpm --filter composable-test-suite run test:ci` — mocked backend (frontend e2e static server +
Playwright RPC fixtures, no backend/login), the in-sandbox bundle injected via `ɵloadPlugin`,
results streamed via console markers, `TEST_FILTER` by identifier substring. The mocked backend is
NOT a limitation for this suite (everything asserted is frontend store logic; empirically
confirmed against the interactive runs). Architecture mirrors `plugin-api-test-suite`'s CI driver;
the mock harness exists in THREE places that must stay in sync (provenance note in `ci/run-ci.ts`).
Details: README, "Running in CI".
## Substrate
`mem:common/test-setup`, `mem:common/component-data-model`, `mem:common/component-swap-pipeline`,
`mem:frontend/testing`.
+7 -1
View File
@@ -23,9 +23,14 @@ From `frontend/`:
- JS lint currently no-ops via `pnpm run lint:js`.
- SCSS lint: `pnpm run lint:scss`.
- Format checks: `pnpm run check-fmt:clj`, `pnpm run check-fmt:js`, `pnpm run check-fmt:scss`.
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`.
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`. After running `fmt:*`, `check-fmt:*` is redundant.
- Translation formatting after i18n edits: `pnpm run translations`.
**Before linting:** if delimiter errors are suspected (after LLM edits, or
lint/compiler reports syntax errors), run `scripts/paren-repair` on the
affected files first. Delimiter errors produce misleading linter output.
See `mem:scripts/paren-repair`.
## Focused memory routing
UI and packages:
@@ -47,6 +52,7 @@ Diagnostics and validation:
- Source-edit compile/hot-reload diagnostics: `mem:frontend/compile-diagnostics`.
- Runtime crash recovery: `mem:frontend/handling-crashes`.
- Tests and live verification: `mem:frontend/testing`.
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Real pointer/keyboard gesture reproduction: `mem:frontend/playwright-gestures`.
## Areas without focused memories
@@ -10,6 +10,12 @@ You have access to two tools for finding errors in Clojure source code (which yo
The latter is needed because syntax errors in parentheses give an uninformative compiler error, and the second
tool can often find the exact location of such errors.
When delimiter errors are detected (typically from lint or compiler output),
fix the affected files with `scripts/paren-repair`. The `clj_check_parentheses`
MCP tool can also pinpoint the error location when available, but it is not
required — standard build errors are usually enough.
See `mem:scripts/paren-repair`.
## Runtime patching with `set!`
Some frontend vars are deliberately mutable escape hatches for runtime instrumentation or circular-dependency patching.
+6 -3
View File
@@ -4,15 +4,18 @@ Frontend validation: CLJS + React/Rumext + RxJS/Potok; SCSS modules; shared CLJC
## Unit tests
READ `mem:testing` FIRST — it defines the execution discipline (no piping, tee to file, preferred commands) that applies to all CLJS/JS test runs.
Frontend unit tests live under `frontend/test/frontend_tests/` and use `cljs.test`. They should be deterministic, avoid DOM/UI integration where possible, and mock side effects such as RPC, storage, timers, or network access.
From `frontend/`:
- Full unit test run: `pnpm run test:quiet`.
- Full unit test run (always builds, suppressed output): `pnpm run test:quiet`.
- Full unit test run (always builds, build output visible): `pnpm run test`.
- Focus a frontend CLJS test namespace: `pnpm run test:quiet -- --focus frontend-tests.logic.components-and-tokens`.
- Focus one frontend CLJS test var: `pnpm run test:quiet -- --focus frontend-tests.logic.components-and-tokens/change-spacing-token-in-main-updates-copy-layout`.
- Quiet `app.*` logging during a run: append `--log-level warn` (or `trace|debug|info|warn|error`).
- Build test target only: `pnpm run build:test`.
- After `pnpm run build:test`, direct compiled runner focus is faster: `node target/tests/test.js --focus frontend-tests.logic.components-and-tokens/change-spacing-token-in-main-updates-copy-layout`.
- Build test target only (no run): `pnpm run build:test`.
- After `build:test` has been run, run the compiled runner directly: `node target/tests/test.js [--focus ...] [--log-level ...]`.
- Watch tests: `pnpm run watch:test`.
New frontend test namespaces must be required/listed in `frontend_tests/runner.cljs`; new vars in existing namespaces need no runner change.
+1
View File
@@ -7,6 +7,7 @@
- Source: `library/src/`; tests: `library/test/`; experimentation/docs: `playground/`, `docs/`; config: `shadow-cljs.edn`, `deps.edn`, `package.json`.
- From `library/`: build `pnpm run build`; bundle helper `pnpm run build:bundle` or `./scripts/build`; tests `pnpm run test`; watch `pnpm run watch` / `pnpm run watch:test`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
- When changing file-format construction or export behavior in `common/`, consider whether `@penpot/library` should be tested because it constructs Penpot files outside the app UI.
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
## JS API and builder state
+1
View File
@@ -85,6 +85,7 @@ From the `mcp/` directory, run
* `pnpm run build` to test the build of all packages
* `pnpm run fmt` to apply the auto-formatter
* Cross-cutting testing principles and anti-patterns: `mem:testing`.
## Devenv plugin/server wiring
+100
View File
@@ -0,0 +1,100 @@
# Media Processor
Stateless HTTP service for Penpot image and font processing. Handles image info extraction, thumbnail generation (sharp), and font conversion (FontForge, woff-tools).
## Tech Stack
- Language: TypeScript
- Runtime: Node.js
- Framework: Express
- Image processing: sharp (libvips)
- Font processing: FontForge (TTF/OTF), sfnt2woff, woff2_decompress
- Upload handling: multer (hybrid storage: memory for small, disk for large)
- Logging: pino (with optional Loki transport)
- Config validation: Zod
- Testing: Vitest
- Package Manager: pnpm
## Project Structure
```
media-processor/
├── src/
│ ├── index.ts # Express app setup, routes, middleware
│ ├── config.ts # Zod-validated env config, HKDF key derivation
│ ├── types.ts # TypeScript type definitions
│ ├── upload.ts # Multer configuration, getFileBuffer helper
│ ├── upload-storage.ts # Hybrid storage engine (memory < threshold, disk >= threshold)
│ ├── logger.ts # Pino logger setup
│ ├── middleware/
│ │ ├── auth.ts # Timing-safe shared key authentication
│ │ ├── error-handler.ts # ProcessingError class, centralized error handling
│ │ └── timeout.ts # Request timeout middleware
│ ├── routes/
│ │ ├── health.ts # GET /api/health
│ │ ├── image.ts # POST /api/image/info, /api/image/thumbnail
│ │ └── font.ts # POST /api/font/convert
│ └── services/
│ ├── image.ts # sharp-based image info/thumbnail generation
│ ├── font.ts # FontForge/woff-tools font conversion
│ └── errors.ts # throwValidation, throwRestriction, throwProcessing
├── test/ # Vitest test files
├── vitest.config.ts # Test configuration
├── tsconfig.json # TypeScript configuration
├── esbuild.config.mjs # Build configuration
└── package.json # Dependencies and scripts
```
## Key Conventions
### Auth
- Requests authenticated via `x-shared-key` header using timing-safe comparison
- When no key configured, all requests rejected with 403
- Key derived from `PENPOT_SECRET_KEY` via HKDF (blake2b512) or set directly via `PENPOT_MEDIA_PROCESSOR_SHARED_KEY`
### Resource Limits
- Image: max pixels, max width/height enforced before processing
- Font: prlimit wraps FontForge processes with memory (AS) and CPU time limits
- Concurrency: p-queue limits concurrent requests (default 10)
- Upload: hybrid storage — memory for files < 10MB, disk for larger; configurable via `PENPOT_MEDIA_PROCESSOR_MEMORY_THRESHOLD`
- Max file size: configurable (default 350MB)
### Error Handling
- `throwValidation(code, hint)` — 400 errors for invalid input
- `throwRestriction(code, hint)` — 413 errors for resource limits exceeded
- `throwProcessing(code, hint)` — 503 errors for processing failures (e.g., resource limit kills)
### Image Processing
- EXIF orientation applied before dimension validation and thumbnail generation
- sharp caching disabled to prevent unbounded memory growth
- `withoutEnlargement: true` prevents upscaling small images
### Font Conversion
- Supported formats: TTF, OTF, WOFF, WOFF2
- SFNT type detected via magic bytes (0x4f54544f = OTF, 0x00010000 = TTF)
- Temp files cleaned up in finally blocks (best-effort)
## Commands
All commands run from `media-processor/` directory:
- `pnpm run test` — Run Vitest test suite
- `pnpm run types:check` — TypeScript type checking (tsc --noEmit)
- `pnpm run fmt` — Format code with Prettier
- `pnpm run fmt:check` — Check formatting without modifying
- `pnpm run build` — Build for production (esbuild)
- `pnpm run start:dev` — Start development server (tsx)
## Docker
- Exposed port: 6065 (configurable via `PENPOT_MEDIA_PROCESSOR_PORT`)
- Must be deployed on internal Docker network only (not public-facing)
- Backend communicates via `PENPOT_MEDIA_PROCESSING_SERVICE_URI`
## Testing Principles
Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Run `pnpm run test` after changes
- Run `pnpm run types:check` after TypeScript changes
- Run `pnpm run fmt:check` before commits
+1
View File
@@ -13,6 +13,7 @@
- From `plugins/`: install `pnpm -r install`; runtime dev server `pnpm run start` or `pnpm run start:app:runtime`; sample plugin `pnpm run start:plugin:<name>`; build runtime `pnpm run build:runtime`; build plugins `pnpm run build:plugins`; lint `pnpm run lint`; format `pnpm run format:check` / `pnpm run format`; tests `pnpm run test`; e2e `pnpm run test:e2e`.
- If a change affects public Plugin API types or runtime, update `plugins/CHANGELOG.md`. Prefix type/signature entries with `**plugin-types:**`; runtime behavior entries with `**plugin-runtime:**`.
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
- JS Plugin API behavior inside Penpot app: `mem:frontend/plugin-api-to-cljs-binding`; TS declarations are not runtime code; many API objects are CLJS proxies in `frontend/src/app/plugins/*.cljs`.
## Sandbox and global cleanup
+1
View File
@@ -26,6 +26,7 @@ From `render-wasm/`:
- Build/copy frontend artifacts: `./build`.
- Watch rebuild: `./watch`.
- Rust tests: `./test` or `cargo test <name>`.
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Lint: `./lint`.
- Format check: `cargo fmt --check`.
+289
View File
@@ -0,0 +1,289 @@
# Error Reports CLI Tool
`scripts/error-reports.mjs` is a Node.js CLI tool for querying Penpot error reports via the RPC API. Provides access to error logs with filtering, pagination, and multiple output formats.
## When to use
- Querying error reports from the database for debugging or analysis
- Filtering errors by source, kind, tenant, or backend version
- Exporting error data in JSON, NDJSON, or table format
- Computing error statistics (top signatures, version, source, audit-log kind, hourly distribution, bursts, heatmap)
- Investigating specific error reports by ID
## Prerequisites
- Node.js with `commander` and `dotenv` packages installed (in root `package.json`)
- Running Penpot backend with error-reports RPC endpoints
- Access token with `error-reports:read` permission
## Configuration
Create a `.env` file in the project root:
```bash
PENPOT_API_URI=http://localhost:3450
PENPOT_ACCESS_TOKEN=<your-token>
```
Grant the required permission to your access token:
```sql
UPDATE access_token
SET perms = ARRAY['error-reports:read']::text[],
updated_at = now()
WHERE id = '<token-uuid>';
```
## Usage
```bash
./scripts/error-reports.mjs <command> [options]
```
### Commands
#### `list` - List error reports with pagination and filters
```bash
./scripts/error-reports.mjs list [options]
```
**Options:**
| Flag | Description | Default |
|------|-------------|---------|
| `-l, --limit <n>` | Max items per page (max: 200) | `50` |
| `--from <date>` | ISO timestamp — oldest boundary (items after this) | — |
| `--to <date>` | ISO timestamp — newest boundary (items before this) | — |
| `--since <date>` | ISO timestamp — explicit cursor for manual pagination | — |
| `--since-id <uuid>` | Fetch errors after this ID (cursor pagination) | — |
| `-s, --source <name>` | Filter by source (see source names below) | — |
| `-p, --profile-id <uuid>` | Filter by profile ID | — |
| `-k, --kind <kind>` | Filter by kind (string) | — |
| `-t, --tenant <tenant>` | Filter by tenant (string) | — |
| `--version <version>` | Filter by version | — |
| `--hint <text>` | Filter by hint (ILIKE match) | — |
| `-a, --all` | Fetch all pages automatically (streams output) | `false` |
| `-f, --format <type>` | Output format: `json`, `table`, or `ndjson` | `table` |
| `--normalize-hints` | Normalize hints by stripping dynamic values | `false` |
| `-o, --output <file>` | Write output to file instead of stdout | — |
| `--env <path>` | Custom .env file path | `.env` |
| `-h, --help` | Show help message | — |
**Streaming behavior:** With `--all`, output must be `ndjson` or `table`; `--all --format json` is rejected because `--all` streams output. `--all --format table` prints rows immediately. `--format ndjson` always streams one JSON object per line.
#### `get` - Get a single error report by ID
```bash
./scripts/error-reports.mjs get [options]
```
**Options:**
| Flag | Description | Required |
|------|-------------|----------|
| `--id <uuid>` | Error report ID | Yes (or --error-id) |
| `--error-id <id>` | Error report error-id | Yes (or --id) |
| `-f, --format <type>` | Output format: `json` or `table` | No (default: `table`) |
| `--env <path>` | Custom .env file path | No (default: `.env`) |
| `-h, --help` | Show help message | No |
#### `stats` - Compute error report statistics
```bash
./scripts/error-reports.mjs stats [options]
```
Reads from `--input <file>`, stdin (piped), or fetches from API. Computes aggregations by signature, version, source, audit-log kind, hour, optional 5-minute bursts, and optional day-of-week × hour heatmap.
**Options:**
| Flag | Description | Default |
|------|-------------|---------|
| `--from <date>` | Start of interval (ISO timestamp) | — |
| `--to <date>` | End of interval (ISO timestamp) | — |
| `--limit <n>` | Items per page when fetching from API | `200` |
| `--input <file>` | Read from local JSON/NDJSON file instead of API | — |
| `--burst` | Detect 5-minute windows above 3× the average rate | `false` |
| `--heatmap` | Show day-of-week × hour-of-day heatmap | `false` |
| `-f, --format <type>` | Output format: `json` or `table` | `table` |
| `--env <path>` | Custom .env file path | `.env` |
## Source Names
The `--source` filter accepts these values:
- `logging`
- `audit-log`
- `rlimit`
## Hint Normalization
With `--normalize-hints` (or always in `stats`), hints are normalized by stripping dynamic values:
1. File IDs in file-id context → `<file-id>`
2. UUIDs (8-4-4-4-12 hex) → `<uuid>`
3. Numeric IDs in parentheses `(12345)``(<id>)`
4. Elapsed times (`7.5s`, `2m3.027s`) → `<elapsed>`
5. URIs (`https://...`) → `<uri>`
6. Unicode quotes and whitespace normalized
## Examples
### List recent errors
```bash
./scripts/error-reports.mjs list --limit 10
```
### Time-range query (today)
```bash
./scripts/error-reports.mjs list --from 2026-07-23T00:00:00Z --to 2026-07-23T23:59:59Z --all
```
### Stream all errors as NDJSON
```bash
./scripts/error-reports.mjs list --all --format ndjson > errors.ndjson
```
### Save to file with --output
```bash
./scripts/error-reports.mjs list --all --format ndjson -o errors.ndjson
./scripts/error-reports.mjs list --format json -o errors.json
```
### Filter by source
```bash
./scripts/error-reports.mjs list --source audit-log --limit 20
```
### Filter by kind
```bash
./scripts/error-reports.mjs list --kind exception-page
```
### Filter by tenant
```bash
./scripts/error-reports.mjs list --tenant production
```
### Filter by version
```bash
./scripts/error-reports.mjs list --version 2.1.0
```
### Search by hint (partial match)
```bash
./scripts/error-reports.mjs list --hint "NullPointerException"
```
### Fetch all errors with pagination
```bash
./scripts/error-reports.mjs list --all
```
### Get specific error by ID
```bash
./scripts/error-reports.mjs get --id 550e8400-e29b-41d4-a716-446655440000
```
### Output as JSON
```bash
./scripts/error-reports.mjs list --limit 5 --format json
```
### Combine filters
```bash
./scripts/error-reports.mjs list --source audit-log --kind exception-page --tenant production --limit 50
```
### Stats with burst and heatmap analysis
```bash
./scripts/error-reports.mjs stats --from 2026-07-23T00:00:00Z --to 2026-07-23T23:59:59Z --burst --heatmap
```
### Stats from file
```bash
./scripts/error-reports.mjs stats --input errors.json
```
### Stats from pipe
```bash
./scripts/error-reports.mjs list --all --format json | ./scripts/error-reports.mjs stats
```
## Output Formats
### Table (default)
Human-readable table format for terminal display. With `--all`, rows stream as they arrive.
### JSON
Single page: `{items: [...], nextSince, nextId}`. `--all` cannot be combined with `--format json`; use `--format ndjson` for streaming.
### NDJSON
One JSON object per line, always streaming. Pipe-friendly: `| jq -c '.hint'`, `| wc -l`.
## Pagination
The server returns items in **ascending** order (oldest first). Cursor pagination uses `--since` / `--since-id` to fetch the next page of newer items.
### Manual pagination
Use `--since` and `--since-id` with values from `nextSince` and `nextId` in the response:
```bash
./scripts/error-reports.mjs list --limit 50
# Use nextSince and nextId from response
./scripts/error-reports.mjs list --limit 50 --since "2026-01-20T10:29:00Z" --since-id "next-uuid"
```
### Automatic pagination
Use `--all` to fetch all pages automatically (streams output):
```bash
./scripts/error-reports.mjs list --all
```
### Time-range queries
Use `--from` and `--to` to bound the query. These map to the server's `--since` and `--until` parameters:
```bash
./scripts/error-reports.mjs list --from 2026-07-20T00:00:00Z --to 2026-07-23T23:59:59Z --all
```
## Key principles
- **Authentication required** - Uses access token with `error-reports:read` permission
- **API endpoint configurable** - Set via `PENPOT_API_URI` in `.env` file
- **Table is default format** - Use `--format json` for structured JSON, `--format ndjson` for streaming
- **Streaming with --all** - Items print as they arrive, no buffering. Use `--format ndjson` or `--format table`; `--all --format json` is rejected.
- **Filters are combinable** - All filter options can be used together
- **Both flag formats supported** - `--option=value` and `--option value` both work
- **Ascending order** - Server returns oldest items first (changed from DESC)
## Error handling
The tool provides helpful error messages for common issues:
- **Missing configuration**: Shows setup instructions for `.env` file
- **Authentication errors (401)**: Indicates invalid or expired token
- **Authorization errors (403)**: Indicates missing `error-reports:read` permission
- **RPC errors**: Displays error code and message from the API
## Integration with other scripts
- **jq**: Pipe NDJSON output to `jq` for further processing
```bash
./scripts/error-reports.mjs list --all --format ndjson | jq -c '{id, hint}'
```
- **stats from pipe**: Fetch data once, compute stats
```bash
./scripts/error-reports.mjs list --all --format ndjson | ./scripts/error-reports.mjs stats
```
- **stats from NDJSON pipe**: Works with NDJSON format too
```bash
./scripts/error-reports.mjs list --all --format ndjson | ./scripts/error-reports.mjs stats
```
- **grep/search**: Filter output by specific patterns
- **--output**: Save to file without shell redirection
```bash
./scripts/error-reports.mjs list --all --format ndjson -o errors.ndjson
```
+80
View File
@@ -0,0 +1,80 @@
# GitHub operations helper
`scripts/gh.py` is a multi-purpose CLI for querying the penpot/penpot GitHub
repository via GraphQL and REST APIs through the authenticated `gh` CLI.
## When to use
- Listing issues in a milestone (for changelog generation).
- Finding issues with no milestone.
- Fetching PR details by number or by milestone.
- Comparing milestone issues against CHANGES.md to find missing entries.
## Prerequisites
- `gh` CLI authenticated (`gh auth status`).
- Python 3.8+.
## Subcommands
### `issues`
List issues in a milestone, with filtering by state, labels, and project status.
```bash
# Closed issues in a milestone (default)
python3 scripts/gh.py issues "2.16.0"
# All issues in a milestone
python3 scripts/gh.py issues "2.16.0" --state all
# Issues with no milestone
python3 scripts/gh.py issues none
python3 scripts/gh.py issues none --state open
# Filter by label (include only)
python3 scripts/gh.py issues "2.16.0" --label "bug"
python3 scripts/gh.py issues "2.16.0" --label "bug,regression"
# Exclude by label
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
# Show only issues NOT yet in CHANGES.md
python3 scripts/gh.py issues "2.16.0" --compare CHANGES.md
```
**Default filters** (override with flags):
- Issues with type "Task" are excluded (`--include-tasks` to keep them).
- Issues with "Rejected" project status are excluded (`--include-rejected` to keep them).
**Output**: JSON array to stdout; progress to stderr.
### `prs`
Fetch PR details by number or by milestone.
```bash
# Fetch specific PRs
python3 scripts/gh.py prs 9179 9204 9311
# Read PR numbers from file
python3 scripts/gh.py prs --file prs.txt
# Read PR numbers from stdin
cat prs.txt | python3 scripts/gh.py prs --stdin
# All PRs in a milestone (default: merged only)
python3 scripts/gh.py prs --milestone "2.16.0"
# All PRs in a milestone (all states)
python3 scripts/gh.py prs --milestone "2.16.0" --state all
```
**Output**: JSON array to stdout; progress to stderr.
## Key principles
- All output is JSON — pipe into `jq` or other tools for further processing.
- Milestone lookup is by exact title match.
- `issues` subcommand auto-paginates (100 items per page).
- `prs` subcommand batches PR number lookups (50 per GraphQL query).
+148
View File
@@ -0,0 +1,148 @@
# nREPL Eval
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
`scripts/nrepl-eval.mjs` — a standalone CLI application.
Session state (defs, in-ns, etc.) persists across invocations via a stored
session ID, so you can build up state incrementally.
## Usage
```bash
node scripts/nrepl-eval.mjs [options] [<code>]
# or
./scripts/nrepl-eval.mjs [options] [<code>]
```
## Options
| Flag | Description | Default |
|------|-------------|---------|
| `--backend` | Connect to backend nREPL (port 6064) | — |
| `--frontend` | Connect to frontend nREPL (port 3447) | — |
| `-p, --port PORT` | nREPL server port | `6064` |
| `-H, --host HOST` | nREPL server host | `127.0.0.1` |
| `-t, --timeout MS` | Timeout in milliseconds | `120000` |
| `--reset-session` | Discard stored session and start fresh | — |
| `-e, --last-error` | Evaluate `*e` to retrieve the last exception | — |
| `-h, --help` | Show help message | — |
- `--backend` and `--frontend` are mutually exclusive.
- Explicit `--port` is overridden when `--backend`/`--frontend` is used.
## When to Use
1. **Evaluate Clojure code** during development — test functions, inspect
state, or run experiments against a running Clojure process.
2. **Verify that edited files compile** — require namespaces with `:reload`
to pick up changes.
3. **Inspect the last exception** after a failed evaluation — use `-e` to
print the error stored in `*e`.
## Workflow
### Session management
Sessions are persisted to `/tmp/penpot-nrepl-session-<host>-<port>`. State
carries across calls automatically:
```bash
./scripts/nrepl-eval.mjs '(def x 42)'
./scripts/nrepl-eval.mjs 'x'
# => 42
```
Reset the session to start fresh:
```bash
./scripts/nrepl-eval.mjs --reset-session '(def x 0)'
```
### Evaluate code
**Single expression (inline) — uses default port 6064:**
```bash
./scripts/nrepl-eval.mjs '(+ 1 2 3)'
```
**Backend nREPL (explicit):**
```bash
./scripts/nrepl-eval.mjs --backend '(+ 1 2 3)'
```
**Frontend nREPL:**
```bash
./scripts/nrepl-eval.mjs --frontend '(js/alert "hi")'
```
**Multiple expressions via heredoc (recommended — avoids escaping issues):**
```bash
./scripts/nrepl-eval.mjs <<'EOF'
(def x 10)
(+ x 20)
EOF
```
**Override with a different port:**
```bash
./scripts/nrepl-eval.mjs -p 7888 '(+ 1 2 3)'
```
### Inspect last exception
After code throws an error, retrieve the full exception details:
```bash
./scripts/nrepl-eval.mjs -e
```
## Common Patterns
**Require a namespace with reload:**
```bash
./scripts/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)"
```
**Test a function:**
```bash
./scripts/nrepl-eval.mjs "(ns/my-function arg1 arg2)"
```
**Long-running operation with custom timeout:**
```bash
./scripts/nrepl-eval.mjs -t 300000 "(long-running-fn)"
```
### Accessing Private Functions
Private functions (declared with `^:private` or `defn-`) cannot be called
directly from outside their namespace. Use the var quote syntax `#'` to
access the underlying var:
**This fails:**
```bash
./scripts/nrepl-eval.mjs "(app.rpc.commands.error-reports/build-list-query {})"
# => Syntax error: app.rpc.commands.error-reports/build-list-query is not public
```
**This works:**
```bash
./scripts/nrepl-eval.mjs "(#'app.rpc.commands.error-reports/build-list-query {})"
# => Returns the result
```
The `#'` reader macro resolves to `(var ...)`, giving you direct access to
the var regardless of its visibility modifier. The syntax is `#'` followed
by the fully qualified symbol.
## Key Principles
- **Default port is 6064** — just pass code directly, no `-p` needed when
your nREPL server is on 6064. Use `--backend` (6064) or `--frontend` (3447)
as quick aliases. Use `-p <PORT>` for any other port.
- **Always use `:reload`** when requiring namespaces to pick up file changes.
- **Session is reused** across invocations — defs, in-ns, and var bindings
persist. Use `--reset-session` to clear.
- **Do not start any server** — the tool connects to an existing nREPL
server, it is not the agent's responsibility to start the nREPL server
(assume the server is already running on the specified port).
+43
View File
@@ -0,0 +1,43 @@
# Paren-Repair
`scripts/paren-repair` fixes mismatched parentheses, brackets, and braces in
Clojure/ClojureScript files, then reformats them with cljfmt.
## When to use
- After LLM edits introduce broken delimiters — proactively run it on files
you just touched.
- When lint (clj-kondo), the Clojure compiler, or shadow-cljs report syntax
errors mentioning mismatched/unclosed delimiters, reader errors, or
unexpected EOF.
- Before running lint/format checks — delimiter errors make linter output
misleading. Fix them first, then lint.
## How to use (CLI)
```bash
# File mode (in-place fix + format)
bb scripts/paren-repair path/to/file.clj
# Pipe mode (stdin → fixed code to stdout)
echo '(def x 1' | bb scripts/paren-repair
# Help
bb scripts/paren-repair --help
```
`bb` must be invoked from the repo root so the path `scripts/paren-repair` resolves.
## Native Tool Available (opencode)
A native opencode tool `paren-repair` is available at `.opencode/scripts/paren-repair.ts`.
The LLM can call it directly with:
- `files`: Array of file paths to fix
- `code`: Code string to fix via stdin
Example usage by the LLM:
```
paren-repair(files="src/foo.clj, src/bar.cljs")
paren-repair(code="(defn foo [x")
```
+39
View File
@@ -0,0 +1,39 @@
# Psql
`scripts/psql` is a wrapper around `psql` that connects to the Penpot PostgreSQL
database using environment variables (`PENPOT_DB_HOST`, `PENPOT_DB_USER`,
`PENPOT_DB_PASSWORD`, `PENPOT_DB_NAME`) with sensible defaults for local
development.
## When to use
- Running ad-hoc SQL queries against the Penpot database.
- Inspecting schema, migrations, or data during development or debugging.
## How to use (CLI)
```bash
# Default connection (penpot db, localhost)
scripts/psql -c "SELECT version();"
# Test database
scripts/psql --test -c "SELECT * FROM migrations;"
# Custom host/user/database
scripts/psql --host myhost --user myuser --db mydb
```
`scripts/psql` must be invoked from the repo root so the path resolves.
## Native Tool Available (opencode)
A native opencode tool `penpot-psql` is available. The LLM can call it directly
with:
- `sql`: SQL command string to execute
- `test`: Boolean flag to use the `penpot_test` database
Example usage by the LLM:
```
penpot-psql(sql="SELECT version();")
penpot-psql(sql="SELECT * FROM migrations;", test=true)
```
+44
View File
@@ -0,0 +1,44 @@
# Taiga API client
`scripts/taiga.py` fetches public issues, user stories, and tasks from the
Penpot Taiga project (id 345963) without authentication.
## When to use
- Fetching details of a Taiga issue, user story, or task by URL or ref number.
- Inspecting status, assignee, tags, description, and other metadata.
- Piping structured JSON into other scripts (with `--json`).
## How to use
```bash
# Fetch by full Taiga URL
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
# Fetch by type and ref number
python3 scripts/taiga.py issue 13714
python3 scripts/taiga.py us 14128
python3 scripts/taiga.py task 13648
# Output raw JSON instead of formatted summary
python3 scripts/taiga.py --json issue 13714
python3 scripts/taiga.py --json https://tree.taiga.io/project/penpot/us/14128
```
## Supported types
| Type | Description |
|------|-------------|
| `issue` | Bug reports, feature requests |
| `us` | User stories |
| `task` | Implementation tasks |
## Output
Default output is a formatted summary with title, status, assignee, author,
tags, URL, and description. Use `--json` for the raw API response.
## Prerequisites
- Python 3.8+ (standard library only, no dependencies).
- Network access to `api.taiga.io`.
+178
View File
@@ -0,0 +1,178 @@
# Testing
## Overview
Tests are proof that code works. Every behavior change needs a test.
Testing in this monorepo varies by module. Each module has its own test
commands, helpers, runner registration requirements, and conventions. This
memory covers cross-cutting testing principles. For module-specific commands
and helpers, consult:
- `mem:common/testing` — CLJC unit tests (JVM + JS), test helpers, fixture
builders, production-path change helpers
- `mem:frontend/testing` — CLJS unit tests, Playwright E2E integration tests,
live browser verification via nREPL
- Backend — JVM `clojure.test` under `backend/test/`; see `mem:backend/core`
## When to Use
- Implementing new logic or behavior
- Fixing any bug (reproduction test required)
- Modifying existing functionality
- Adding edge case handling
**When NOT to use:** Pure configuration changes, documentation updates, or
static content changes with no behavioral impact.
## TDD: Recommended Workflow
Write a failing test before writing the code that makes it pass. For bug fixes,
reproduce the bug with a test before attempting a fix.
When TDD isn't practical (exploratory work, tight coupling to unknown APIs),
still write tests before considering the work complete.
```
RED GREEN REFACTOR
Write a test Write minimal code Clean up the
that fails ──→ to make it pass ──→ implementation ──→ (repeat)
│ │ │
▼ ▼ ▼
Test FAILS Test PASSES Tests still PASS
```
- **RED** — Write the test first. It must fail. A test that passes immediately
proves nothing.
- **GREEN** — Write the minimum code to make the test pass. Don't over-engineer.
- **REFACTOR** — With tests green, improve the code without changing behavior:
extract shared logic, improve naming, remove duplication. Run tests after
every step.
## The Prove-It Pattern (Bug Fixes)
When a bug is reported, **do not start by trying to fix it.** Start by writing
a test that reproduces it:
1. Write a test that demonstrates the bug
2. Confirm the test FAILS (proving the bug exists)
3. Implement the fix
4. Confirm the test PASSES (proving the fix works)
5. Run the full test suite for the module (no regressions)
## Core Principles
- **Test State, Not Interactions** — assert on outcomes, not method calls;
survives refactoring
- **DAMP over DRY** — tests are specifications; duplication is OK if each test
is self-contained and readable. A test should tell a complete story without
requiring the reader to trace through shared helpers.
- **Prefer Real Implementations** — hierarchy: Real > Fake > Stub > Mock;
mock only at boundaries (network, RPC, filesystem, email)
- **Arrange-Act-Assert** — every test: setup / action / verify
- **One Assertion Per Concept** — each test verifies one behavior; split
compound assertions
- **Descriptive Test Names** — names read like specifications
## Prefer Real Implementations Over Mocks
Work down this list:
1. **Real implementation** — Test the actual code with real collaborators.
Highest confidence.
2. **Fake** — A simplified but functional in-memory implementation (e.g.
atom/dict-backed store instead of a real database).
3. **Stub** — Returns canned data. Use when the collaborator's logic is
irrelevant.
4. **Mock** — Last resort, only at boundaries. Use only when verifying
interaction with an external system that cannot be faked.
**Rule of thumb:** If you can write a fake or use the real implementation, do
that. If you find yourself asserting on call counts or invocation order, ask
whether a fake would be clearer.
## Fixtures over Manual Setup
Use fixture/`beforeEach` mechanisms for shared setup and teardown. Each test
should own its state so tests don't interfere with each other. Shorter-scope
fixtures (`:each` / per-test) are preferred; longer-scope fixtures (`:once` /
suite-level) are only for expensive, immutable shared setup.
## Parametrized Tests
Use your test framework's parametrize/table-driven mechanism to test multiple
scenarios with a single test body. Keeps tests concise and surfaces all cases
at a glance.
## Test Pyramid
```
╱╲
╲ E2E (few)
╲ Full flows, real browser/server
╱──────╲
╲ Integration (some)
╲ Cross-module, test DB
╱────────────╲
╱ ╲ Unit (most)
╱ ╲ Pure logic, fast
╱──────────────────╲
```
Prefer unit tests for pure logic. Reach for integration/E2E tests when covering
RPC handlers, database queries, or full user flows. In the frontend, Playwright
E2E tests should not be added unless explicitly requested.
## Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Testing implementation details | Breaks on refactor | Test inputs/outputs |
| Flaky tests (timing, order-dependent) | Erodes trust | Deterministic assertions, isolate state |
| Mocking everything | Tests pass, production breaks | Prefer real implementations or fakes |
| No test isolation | Pass individually, fail together | Per-test state fixtures |
| Testing framework/platform code | Wastes time | Only test YOUR code |
| Snapshot abuse | Nobody reviews, break on any change | Focused assertions |
| Skipping tests to make suite pass | Hides real failures | Fix the test or fix the code |
## Execution discipline
**CRITICAL: Test output handling rules**
When running ANY test command (CLJS/JS or JVM):
1. **NEVER pipe test output directly to `| head`, `| tail`, `| grep`, or similar filters** — this can hide failures and cause you to miss critical errors.
2. **ALWAYS pipe to a file first, then read the file:**
```bash
# CORRECT:
pnpm run test 2>&1 > /tmp/test-output.txt
grep -A 5 "failures" /tmp/test-output.txt
# WRONG:
pnpm run test 2>&1 | tail -20
pnpm run test 2>&1 | grep "failures"
```
3. **Use `--focus` to narrow test scope** instead of filtering output.
4. **Read the full output file** to understand test results completely.
When running CLJS/JS tests (frontend, common):
- **Always use `pnpm run test:quiet`** — it silently builds the test bundle then runs the test runner, giving you clean test output.
- Use `pnpm run test` when you want to see build output alongside test results (always builds, then runs).
- After `build:test` has been run once, you can invoke the runner directly: `node target/tests/test.js [--focus ...] [--log-level ...]`.
When running JVM tests (backend, common):
- Use `clojure -M:dev:test` directly (no pnpm wrapper).
- Same file-piping rule applies.
## Verification Checklist
After completing any implementation:
- [ ] Every new behavior has a corresponding test
- [ ] All tests pass for touched modules
- [ ] Bug fixes include a reproduction test that failed before the fix
- [ ] Test names describe the behavior being verified
- [ ] No tests were skipped or disabled
- [ ] Lint/formatter passes for touched modules
- [ ] New test files registered in the module's runner/entrypoint (see module
testing memory)
+11 -1
View File
@@ -14,10 +14,20 @@ automatically pull the identity from the local git config `user.name` and `user.
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
Body explaining what changed and why.
Wrap lines at 72 characters — git log and tooling
render long lines poorly. Keep each line concise.
Co-authored-by: <You (the LLM)>
AI-assisted-by: model-name
```
**AI-assisted-by trailer rules:**
- Use only the model name, e.g. `mimo-v2.5`, `deepseek-v4-flash`
- Do NOT add prefixes like `opencode-go/` — use the bare model name
## Commit Type Emojis
`:bug:` bug fix · `:sparkles:` enhancement · `:tada:` new feature · `:recycle:` refactor · `:lipstick:` cosmetic · `:ambulance:` critical fix · `:books:` docs · `:construction:` WIP · `:boom:` breaking · `:wrench:` config · `:zap:` perf · `:whale:` docker · `:paperclip:` other · `:arrow_up:` dep upgrade · `:arrow_down:` dep downgrade · `:fire:` removal · `:globe_with_meridians:` translations · `:rocket:` epic/highlight
## Referencing Issues
Use `Closes #NNNN` (not `Fixes #NNNN`) to link a commit to a GitHub issue.
+199 -4
View File
@@ -35,7 +35,7 @@ Command what should be built. Format: `[Imperative verb] [what] in/on [where]`.
| Field | Rule |
|-------|------|
| **Labels** | `bug` (crashes/regressions) · `enhancement` (new features) · `community contribution` (PRs from non-core) · skip workflow labels (`backport candidate`, `team-qa`) |
| **Labels** | `community contribution` (PRs from non-core) · skip workflow labels (`backport candidate`, `team-qa`) · do **not** add `bug` or `enhancement` labels (use Issue Type instead) |
| **Milestone** | Use the current or next planned milestone. Fetch available milestones: `gh api repos/penpot/penpot/milestones --jq '.[].title'`. If unsure, omit. |
| **Project** | Always `Main` (project number 8). Use `--project "Main"` flag. |
| **Issue Type** | See Issue Type section below. Cannot be set via `gh issue create` — use GraphQL after creation. |
@@ -79,6 +79,8 @@ Write the body to a temp file to avoid shell quoting issues:
<version>
```
Note: do not soft-wrap paragraphs in the body. Each paragraph is a single line in the source; newlines are reserved for structural breaks (section headers, list items, code-block fences, blank-line separators). List items stay on a single line each. GitHub renders single-line paragraphs correctly, and wrapping makes diffs noisy on every small wording change. Same rule applies to PR bodies.
## Creating the Issue
```bash
@@ -112,8 +114,8 @@ Output: `https://github.com/penpot/penpot/issues/<NUMBER>`
| Docs | `IT_kwDOAcyBPM4B_IQz` |
**Map:**
- `bug` label → Bug
- `enhancement` label → Enhancement
- Bug report (steps to reproduce, expected vs. actual) → Bug
- Enhancement / new feature → Enhancement
- Feature/epic → Feature
- Docs → Docs
- None of the above → Task
@@ -155,6 +157,199 @@ query { repository(owner: "penpot", name: "penpot") {
rm -f /tmp/issue-body.md
```
## Creating Issues from PRs
Used when the project board needs an issue as the primary changelog/release
unit and the PR describes the implementation. The issue is the **WHAT**
(user-facing), the PR is the **HOW** (implementation).
### Fetch the PR
```bash
gh pr view <PR_NUMBER> --repo penpot/penpot \
--json title,body,author,labels,baseRefName,mergedAt,state,milestone
```
Identify:
- **WHAT** — user-facing problem or feature. Goes into the issue.
Describe symptoms and impact, not internal mechanisms.
- **HOW** — implementation details. These belong in the PR, not the issue.
### Determine metadata
- **Title:** rewrite from user perspective using the title rules above. Strip
leading emoji prefixes (`:bug:`, `:sparkles:`, `:tada:`). Focus on
observable behavior.
- **Labels:** copy `community contribution` if present on the PR.
- **Milestone:** always copy what's on the PR.
```bash
gh pr view <PR_NUMBER> --json milestone --jq '.milestone.title'
```
If the PR has no milestone, create the issue without one.
- **Project:** `Main`.
- **Body:** extract the user-facing section (steps to reproduce or feature
description). Omit internal details. Use the templates above.
- **Issue Type:** use the mapping table above (also handles `:bug:` /
`:sparkles:` / `:tada:` title prefixes).
### Create the issue
```bash
cat > /tmp/issue-body.md << 'ISSUE_BODY'
<body content here>
ISSUE_BODY
gh issue create \
--repo penpot/penpot \
--title "<Title>" \
--label "community contribution" \ # only if PR has this label
--milestone "<milestone>" \
--project "Main" \
--body-file /tmp/issue-body.md
```
Output: `https://github.com/penpot/penpot/issues/<NUMBER>`
### Assign to the PR author
```bash
AUTHOR=$(gh pr view <PR_NUMBER> --repo penpot/penpot --json author --jq '.author.login')
gh issue edit <ISSUE_NUMBER> --repo penpot/penpot --add-assignee "$AUTHOR"
```
### Set Issue Type and verify
See the **Setting the Issue Type** and **Verification** sections above — the
GraphQL mutations and `gh issue view` calls are identical regardless of how
the issue was sourced.
### Link the PR to the issue
Append `Closes #<ISSUE_NUMBER>` to the PR body:
```bash
gh pr view <PR_NUMBER> --repo penpot/penpot --json body --jq '.body' > /tmp/pr-body.md
printf "\n\nCloses #<ISSUE_NUMBER>\n" >> /tmp/pr-body.md
gh pr edit <PR_NUMBER> --repo penpot/penpot --body-file /tmp/pr-body.md
# Verify
gh pr view <PR_NUMBER> --repo penpot/penpot --json body \
--jq '.body | test("Closes #<ISSUE_NUMBER>")'
```
**Note:** If the PR is already merged, `Closes` won't auto-close the issue —
it only creates the "Development" sidebar link. This is the desired
behavior since the issue is a tracking artifact.
### Clean up
```bash
rm -f /tmp/issue-body.md /tmp/pr-body.md
```
### Rules for this flow
- **One issue per PR** — even if a PR fixes multiple things, create a single
issue that summarizes the overall change.
- **Community attribution:** if the PR has the `community contribution`
label or the author is not a core team member, add the label to the issue.
- **Don't put implementation details in the issue body** — the issue is for
users, QA, and changelog readers.
## Creating Issues from Draft Body
Used when the user provides a draft body from elsewhere (Taiga story, user
report, discussion transcript) and there is no PR yet.
### Get the body
Read the draft body from wherever it was provided. If the user gives only a
vague one-liner, ask them to expand it (steps to reproduce, expected vs.
actual, use case) before proceeding.
### Derive the title
Apply the title rules in the **Title Derivation** section above. Distinguish
bug vs. feature from the body content:
- Steps to reproduce + expected vs. actual → bug
- "would be nice", "add support for", "allow users to" → feature / enhancement
### Choose a body template
Use the bug or enhancement template from the **Issue Body Template** section
above. Fill in placeholders with the user-provided details. If the body
doesn't fit either, ask the user which template to use.
### Determine metadata
- **Project:** `Main` (always).
- **Milestone:** ask the user if not obvious; otherwise omit.
- **Labels:** usually none for new user-reported issues. Add
`community contribution` if the user is a non-team contributor.
- **Issue Type:** use the mapping table above (bug description → Bug; feature
request → Enhancement or Feature).
### Create the issue
```bash
cat > /tmp/issue-body.md << 'ISSUE_BODY'
<body content here>
ISSUE_BODY
gh issue create \
--repo penpot/penpot \
--title "<Title>" \
--label "community contribution" \ # only if applicable
--milestone "<milestone>" \ # only if provided
--project "Main" \
--body-file /tmp/issue-body.md
```
### Set Issue Type and verify
Same GraphQL mutation and `gh issue view` commands as in the
**Setting the Issue Type** and **Verification** sections above.
### Clean up
```bash
rm -f /tmp/issue-body.md
```
## Retitling an Existing Issue
Used when an issue's current title is vague, prefixed, or no longer matches
the body (e.g. `[PENPOT FEEDBACK]: ...`, `feature: ...`).
### Fetch the issue
```bash
gh issue view <NUMBER> --repo penpot/penpot --json title,body
```
### Derive a new title
Read the body (not the current title) and apply the title rules in the
**Title Derivation** section above.
### Apply the new title
```bash
gh issue edit <NUMBER> --repo penpot/penpot --title "<NEW TITLE>"
```
### Confirm
```bash
gh issue view <NUMBER> --repo penpot/penpot --json title
```
## See Also
- Creating issues **from PRs** (separating WHAT from HOW): `mem:workflow/creating-prs`
- End-to-end orchestration entry point: the `create-issue` skill at
`.opencode/skills/create-issue/SKILL.md`. The skill is a thin entry
point; this memory is the canonical home for all issue-creation rules.
+73 -6
View File
@@ -2,6 +2,20 @@
PR only on explicit request. Branch: issue/feature-specific; fallback `<type>/<short-description>` (`fix/...`, `feat/...`, `refactor/...`, `docs/...`, `chore/...`, `perf/...`).
## Target Branch
Auto-detect the base branch with `scripts/detect-target-branch`:
```bash
TARGET=$(scripts/detect-target-branch)
```
This outputs `staging` or `develop` by walking the local commit graph (pure local, no remote/network). Do not ask the user for the target branch unless the tool fails.
## Metadata
Always add the PR to the Main project (`--project "Main"`) unless the user explicitly requests a different project.
## Title Format
PR titles follow commit title conventions:
@@ -12,21 +26,74 @@ PR titles follow commit title conventions:
See `mem:workflow/creating-commits` for emoji codes. Squash merge uses the PR title as the final commit subject, so title format matters.
## Description
## Description Body
Include concise sections covering:
- what changed and why;
- related GitHub issues or Taiga stories (`Fixes #NNNN`, `Relates to #NNNN`, `Taiga #NNNN`);
- related GitHub issues or Taiga stories (`Closes #NNNN`, `Relates to #NNNN`, `Taiga #NNNN`);
- screenshots or recordings for UI-visible changes;
- testing performed and residual risk;
- breaking changes or migration notes, if any.
PR descriptions are expected to start with:
PR descriptions follow this structure:
> **Note:** This PR was created with AI assistance as part of the Penpot MCP self-improvement initiative.
```markdown
**Note:** This PR was created with AI assistance.
## What
<the problem or feature and its user-facing impact — short bullet items where there is more than one point>
## Why
<root cause or motivation — a short paragraph or bullets>
## How
<high-level approach and key decisions — bullet items, grouped by area (bold lead-ins) for larger PRs>
```
The "Note:" line is required at the top. Adjust if this is a manual (non-AI) PR.
## Writing Principles
- **Write for humans.** The diff shows what changed. The description explains why.
- **Be concise.** Focus on reasoning: What was the problem? Why did it happen? How did you solve it?
- **Prefer bullets over paragraphs.** Short bullet items, grouped by area with bold lead-ins where helpful, are far easier to digest than prose; keep any remaining paragraph to a few sentences.
- **No manual line wraps.** Markdown renders adapting to the viewport; hard-wrapped lines degrade rendering. One line per paragraph or bullet, however long.
- **Skip the obvious.** Don't explain what `git diff` already shows.
### What NOT to Include
- ❌ List of files changed (visible in diff)
- ❌ Testing steps (CI handles this)
- ❌ Screenshots unless UI-visible
- ❌ Migration notes unless breaking changes
- ❌ Regression fixes introduced during the PR (they're part of the development process, not the feature)
## Before Opening
- Follow `mem:workflow/creating-commits` for changelog expectations.
- Follow `mem:workflow/creating-commits` for commits
- Run the focused tests/lints appropriate to touched modules.
- Do not force-push during review unless the maintainer workflow explicitly asks for it.
- Do not force-push during review unless the maintainer workflow explicitly asks for it.
- When the user says the code is already pushed, trust that — do not verify remote branch existence via `git ls-remote` or `git fetch`.
## Creating the PR
```bash
cat > /tmp/pr-body.md << 'PR_BODY'
<body content here>
PR_BODY
TARGET=$(scripts/detect-target-branch)
gh pr create \
--repo penpot/penpot \
--base "$TARGET" \
--head <branch> \
--title "<title>" \
--project "Main" \
--body-file /tmp/pr-body.md
rm -f /tmp/pr-body.md
```
+60 -23
View File
@@ -1,26 +1,31 @@
# the name by which the project can be referenced within Serena
# the name by which the project can be referenced within Serena/when chatting with the LLM.
project_name: "penpot"
# list of languages for which language servers are started; choose from:
# al ansible bash clojure cpp
# cpp_ccls crystal csharp csharp_omnisharp dart
# elixir elm erlang fortran fsharp
# list of languages for which language servers are started (LSP backend only); choose from:
# ada al angular ansible bash
# bsl clojure cpp cpp_ccls crystal
# csharp csharp_omnisharp cue dart elixir
# elm erlang fortran fsharp gdscript
# go groovy haskell haxe hlsl
# java json julia kotlin lean4
# lua luau markdown matlab msl
# nix ocaml pascal perl php
# php_phpactor powershell python python_jedi python_ty
# r rego ruby ruby_solargraph rust
# scala solidity swift systemverilog terraform
# toml typescript typescript_vts vue yaml
# zig
# (This list may be outdated. For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# html java json julia kotlin
# latex lean4 lua luau markdown
# matlab msl nix ocaml pascal
# perl php php_phpactor php_phpantom powershell
# python python_jedi python_pyrefly python_ty r
# rego ruby ruby_solargraph rust scala
# scss solidity svelte swift systemverilog
# terraform toml typescript typescript_vts vue
# yaml zig
# (This list may be outdated; generated with scripts/print_language_list.py;
# For the current list, see values of Language enum here:
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
# Note:
# - For C, use cpp
# - For JavaScript, use typescript
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
# - For Free Pascal/Lazarus, use pascal
# Special requirements:
# Some languages require additional setup/installations.
@@ -54,12 +59,19 @@ ignore_all_files_in_gitignore: true
# advanced configuration option allowing to configure language server-specific options.
# Maps the language key to the options.
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
# No documentation on options means no options are available.
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
ls_specific_settings: {}
# list of additional paths to ignore in this project.
# Same syntax as gitignore, so you can use * and **.
# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
# Example:
# ignored_paths:
# - "examples/**"
# - ".worktrees/**"
# - "**/bin/**"
# - "**/obj/**"
# Note: global ignored_paths from serena_config.yml are also applied additively.
ignored_paths: []
@@ -130,13 +142,38 @@ ignored_memory_patterns: []
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
added_modes:
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
# list of additional workspace folder paths for cross-package reference support.
# Paths can be absolute or relative to the project root.
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
# symbols and references across package boundaries.
# Currently supported for: TypeScript.
# symbols and references across package boundaries, but these folders are not indexed by Serena,
# i.e. the respective symbols will not be found using Serena's symbol search tools.
# Example:
# additional_workspace_folders:
# - ../sibling-package
# - ../shared-lib
additional_workspace_folders: []
ls_additional_workspace_folders: []
# list of workspace folder paths (LSP backend only).
# These folders will be used to build up Serena's symbol index.
# Paths must be within the project root and should thus be relative to the project root.
# Furthermore, the paths should not be filtered by ignore settings.
# Default setting: The entire project root folder (".") is considered.
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
# ls_workspace_folders:
# - "./subproject1"
# - "./subproject2"
ls_workspace_folders:
- .
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
# the command runs in the project root directory and is only executed if the project is trusted
# (see trusted_project_path_patterns in the global configuration).
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
# example: activation_command: "npx nx run-many -t build"
activation_command:
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
# must be a positive number.
activation_command_timeout: 180.0
+37
View File
@@ -1,5 +1,23 @@
# AI AGENT GUIDE
## HARD RULES (always apply — no exceptions)
- **Never `git push`, force-push, or modify `git origin`** (or any other remote).
The user pushes from their own shell. If a push is required to surface the
agent's work (e.g. force-push after an amend), state this in the response and
wait for the user to push. Do not change the remote URL, do not switch SSH↔HTTPS.
- **Never amend a commit that has been pushed** unless the user explicitly asks.
If the user pushes, treat that commit as final from the agent's side.
- **Never pipe test output directly to filters** (`| head`, `| tail`, `| grep`, etc.).
Always redirect to a file first: `command > /tmp/output.txt 2>&1`, then read/grep the file.
This prevents hiding test failures. See `mem:testing` for details.
- **Read the workflow memory BEFORE the corresponding action**:
- Before `git commit``mem:workflow/creating-commits` (commit format, AI-assisted-by trailer)
- Before `gh issue create``mem:workflow/creating-issues` (title derivation, body template, Issue Type)
- Before `gh pr create` / `gh pr edit``mem:workflow/creating-prs` (title format, body structure, AI note)
Don't infer format from the title of a previous commit/issue/PR — the memory
is the source of truth.
## CRITICAL: Read module memories BEFORE writing any code
Do this **before planning, before coding, before touching any file**:
@@ -77,3 +95,22 @@ precision while maintaining a strong focus on maintainability and performance.
down into atomic steps.
2. Be concise and autonomous.
3. Do **not** touch unrelated modules unless the task explicitly requires it.
---
# Available Scripts & Tools
## Native opencode Tools (callable directly by the LLM)
- `paren-repair` — Fix mismatched delimiters + reformat Clojure files. Example: `paren-repair(files="src/foo.clj, src/bar.cljs")`
- `penpot-psql` — Execute SQL against the Penpot database. Example: `penpot-psql(sql="SELECT version();")`
## Scripts (from repo root via `scripts/<name>`)
- `scripts/paren-repair` — Fix mismatched delimiters in Clojure/CLJS files + reformat with cljfmt. See `mem:scripts/paren-repair`.
- `scripts/psql` — Connect to the Penpot PostgreSQL database (wraps `psql` with env-var defaults). See `mem:scripts/psql`.
- `scripts/nrepl-eval.mjs` — Evaluate Clojure code via nREPL (backend + frontend).
- `scripts/check-commit` — Validate commit messages against Penpot's commit guidelines.
- `scripts/check-fmt-clj` — Check Clojure formatting without modifying files.
- `scripts/ci` — CI orchestration script for running lint, tests, and format checks across modules. See `scripts/ci --help`.
+157 -65
View File
@@ -1,17 +1,50 @@
# CHANGELOG
## 2.17.0 (Unreleased)
## 2.18.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :bug: Bugs fixed
### :rocket: Epics and highlights
- Fix MCP integration hanging when the Penpot tab is backgrounded or frozen by the browser [#10323](https://github.com/penpot/penpot/issues/10323) (PR: [#10392](https://github.com/penpot/penpot/pull/10392))
- Fix synced component copy not reflowing children after spacing token update [#9892](https://github.com/penpot/penpot/issues/9892)
- Fix spacebar activating pan mode while typing a comment (by @Krishcode264) [#10285](https://github.com/penpot/penpot/issues/10285) (PR: [#10287](https://github.com/penpot/penpot/pull/10287))
- Fix plugin API rejecting negative letterSpacing values (by @filipsajdak) [#9780](https://github.com/penpot/penpot/issues/9780) (PR: [#10257](https://github.com/penpot/penpot/pull/10257))
- Fix plugin API addTheme calls failing with the signature shown in the high-level overview [#10074](https://github.com/penpot/penpot/issues/10074) (PR: [#10359](https://github.com/penpot/penpot/pull/10359))
- Fix empty text shape not being deleted on editor exit [#10540](https://github.com/penpot/penpot/issues/10540) (PR: [#10541](https://github.com/penpot/penpot/pull/10541))
- Fix broken token pills showing wrong default state when not selected [#10524](https://github.com/penpot/penpot/issues/10524) (PR: [#10535](https://github.com/penpot/penpot/pull/10535))
- Replace hyphens with bullets in subscription benefits list [#10547](https://github.com/penpot/penpot/issues/10547) (PR: [#10523](https://github.com/penpot/penpot/pull/10523))
- Fix Chinese (zh-CN) translation showing wrong label for Intersection in board path menu (by @sawirricardo) [#10346](https://github.com/penpot/penpot/issues/10346) (PR: [#10381](https://github.com/penpot/penpot/pull/10381))
### :sparkles: New features & Enhancements
- Group toolbar drawing tools into shape and free-draw flyouts [#9316](https://github.com/penpot/penpot/issues/9316) (PR: [#9480](https://github.com/penpot/penpot/pull/9480), [#10354](https://github.com/penpot/penpot/pull/10354))
- Add outline stroke to Paths [#9961](https://github.com/penpot/penpot/issues/9961) (PR: [#8677](https://github.com/penpot/penpot/pull/8677))
- Make throwValidationErrors default to true for v2 manifest plugins [#10401](https://github.com/penpot/penpot/issues/10401) (PR: [#10433](https://github.com/penpot/penpot/pull/10433))
- Add dedicated Line and Arrow drawing tools (by @davidv399) [#9145](https://github.com/penpot/penpot/issues/9145) (PR: [#9146](https://github.com/penpot/penpot/pull/9146))
- Refactor wasm rulers and UI state [#10116](https://github.com/penpot/penpot/issues/10116) (PR: [#10461](https://github.com/penpot/penpot/pull/10461))
- Improve team invitations modal in the dashboard [#10484](https://github.com/penpot/penpot/issues/10484) (PR: [#10459](https://github.com/penpot/penpot/pull/10459))
## 2.17.1 (Unreleased)
### :bug: Bugs fixed
- Fix malformed get-font-variants request when team-id is missing from dashboard URL [#10644](https://github.com/penpot/penpot/issues/10644) (PR: [#10645](https://github.com/penpot/penpot/pull/10645))
- Fix malformed get-profiles-for-file-comments request when file-id is missing from workspace URL [#10652](https://github.com/penpot/penpot/issues/10652) (PR: [#10655](https://github.com/penpot/penpot/pull/10655))
- Fix workspace crash when holding an arrow key on a selection due to excessive re-renders [#10726](https://github.com/penpot/penpot/issues/10726) (PR: [#10736](https://github.com/penpot/penpot/pull/10736))
- Fix asset download failing with S3 auth conflict when using access token [#10776](https://github.com/penpot/penpot/issues/10776) (PR: [#10777](https://github.com/penpot/penpot/pull/10777))
- Fix internal error when dragging inner layout with Boolean operations [#10647](https://github.com/penpot/penpot/issues/10647) (PR: [#10778](https://github.com/penpot/penpot/pull/10778))
- Fix viewer crash with WASM panic when opening URL with page-id [#10800](https://github.com/penpot/penpot/issues/10800) (PR: [#10805](https://github.com/penpot/penpot/pull/10805))
## 2.17.0
### :rocket: Epics and highlights
- Render prototype viewer with WASM (Skia) engine instead of SVG [#10037](https://github.com/penpot/penpot/issues/10037) (PR: [#10038](https://github.com/penpot/penpot/pull/10038))
### :sparkles: New features & Enhancements
- Show a read-only W × H size badge below the bounding box of the current selection (by @bittoby) [#9205](https://github.com/penpot/penpot/issues/9205)
- Expose `variants` retrieval on `LibraryComponent` via `isVariant()` type guard in plugin API (by @opcode81) [#9185](https://github.com/penpot/penpot/issues/9185) (PR: [#9302](https://github.com/penpot/penpot/pull/9302))
- Add search bar to prototype interaction destination dropdown (by @EvaMarco) [#8618](https://github.com/penpot/penpot/issues/8618) (PR: [#9769](https://github.com/penpot/penpot/pull/9769))
- Apply styles to selection (by @AzazelN28) [#9661](https://github.com/penpot/penpot/issues/9661) (PR: [#8625](https://github.com/penpot/penpot/pull/8625))
- Add dashed stroke customization with dash and gap inputs (by @EvaMarco) [#3881](https://github.com/penpot/penpot/issues/3881) (PR: [#9765](https://github.com/penpot/penpot/pull/9765))
- Add author, relative timestamp and short identifier to history entries (by @FairyPigDev) [#7660](https://github.com/penpot/penpot/issues/7660) (PR: [#9132](https://github.com/penpot/penpot/pull/9132))
- Add typography token row to multiselected texts [#9336](https://github.com/penpot/penpot/issues/9336) (PR: [#9128](https://github.com/penpot/penpot/pull/9128))
@@ -22,7 +55,7 @@
- Remove unreachable try/catch in hex->hsl (by @Dexterity104) [#9244](https://github.com/penpot/penpot/issues/9244) (PR: [#9245](https://github.com/penpot/penpot/pull/9245))
- Remove stray debug log in exporter upload-resource (by @iot2edge) [#9270](https://github.com/penpot/penpot/issues/9270) (PR: [#9272](https://github.com/penpot/penpot/pull/9272))
- Release pool connection during font variant creation (by @Dexterity104) [#9286](https://github.com/penpot/penpot/issues/9286) (PR: [#9287](https://github.com/penpot/penpot/pull/9287))
- Add autocomplete combobox to token creation and edition forms [#9899](https://github.com/penpot/penpot/issues/9899) (PR: [#9109](https://github.com/penpot/penpot/pull/9109))
- Add autocomplete combobox to token creation and edition forms [#9899](https://github.com/penpot/penpot/issues/9899) (PR: [#9109](https://github.com/penpot/penpot/pull/9109), [#8294](https://github.com/penpot/penpot/pull/8294))
- Add list view mode to color picker UI [#4420](https://github.com/penpot/penpot/issues/4420) (PR: [#9953](https://github.com/penpot/penpot/pull/9953))
- Use Clipboard API consistently across the application (by @MilosM348) [#6514](https://github.com/penpot/penpot/issues/6514) (PR: [#9188](https://github.com/penpot/penpot/pull/9188))
- Use `$` as DTCG token/group discriminator and make `$description` optional [#8342](https://github.com/penpot/penpot/issues/8342) (PR: [#9912](https://github.com/penpot/penpot/pull/9912))
@@ -33,68 +66,39 @@
- Update contributing guidelines with current issue tags and CSS linting rules [#9900](https://github.com/penpot/penpot/issues/9900) (PR: [#9418](https://github.com/penpot/penpot/pull/9418))
- Add composite typography token input to the Design sidebar [#9932](https://github.com/penpot/penpot/issues/9932) (PR: [#9128](https://github.com/penpot/penpot/pull/9128), [#9375](https://github.com/penpot/penpot/pull/9375), [#8749](https://github.com/penpot/penpot/pull/8749))
- Avoid deduplicating temporary export files to prevent stale content (by @yong2bba) [#9970](https://github.com/penpot/penpot/issues/9970) (PR: [#9959](https://github.com/penpot/penpot/pull/9959))
- Add layer blur effect [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))
- Add concurrency limiter for MCP Server Plugin Communications [#9493](https://github.com/penpot/penpot/issues/9493) (PR: [#9748](https://github.com/penpot/penpot/pull/9748))
- Render guides in WebGL [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014))
- Add configurable resource limits to ImageMagick image processing [#10223](https://github.com/penpot/penpot/issues/10223) (PR: [#10240](https://github.com/penpot/penpot/pull/10240))
- Add resource limits to font processing child processes [#10234](https://github.com/penpot/penpot/issues/10234) (PR: [#10274](https://github.com/penpot/penpot/pull/10274))
- Add color variants and positioning to selection size badge [#10258](https://github.com/penpot/penpot/issues/10258) (PR: [#9210](https://github.com/penpot/penpot/pull/9210))
- Use hard reload for render engine switching in the workspace menu [#10441](https://github.com/penpot/penpot/issues/10441) (PR: [#10444](https://github.com/penpot/penpot/pull/10444))
- Rotate size badge when shape is rotated [#10386](https://github.com/penpot/penpot/issues/10386) (PR: [#10393](https://github.com/penpot/penpot/pull/10393))
- Add separate internal URI for exporter to handle Docker deployments where internal and public URIs differ [#10627](https://github.com/penpot/penpot/issues/10627) (PR: [#10630](https://github.com/penpot/penpot/pull/10630))
### :bug: Bugs fixed
- Fix plugin API `fileVersion.restore()` promise hanging indefinitely on restore failure [Github #9092](https://github.com/penpot/penpot/issues/9092)
- Fix LDAP provider params schema typo (`bind-passwor``bind-password`) introduced during the `clojure.spec``malli` migration; the schema slot now matches the runtime key actually read by `prepare-params` (`:password (:bind-password cfg)`) and `try-connectivity` (`(:bind-password cfg)`), so a wrong type for the password no longer slips through unvalidated
- Fix `login-with-ldap` silently dropping its error message on the `ldap-not-initialized` restriction (typo `:hide``:hint`); the message `"ldap auth provider is not initialized"` now actually surfaces in logs and error responses instead of being discarded into an unread key
- Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108)
- Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD
- Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877)
- Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838)
- Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947)
- Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582)
- Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526)
- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
- Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924)
- Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639)
- Fix dates to avoid show them in english when browser is in auto [Taiga #13786](https://tree.taiga.io/project/penpot/issue/13786)
- Fix focus radio button [Taiga #13841](https://tree.taiga.io/project/penpot/issue/13841)
- Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631)
- Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906)
- Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864)
- Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871)
- Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923)
- Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930)
- Fix dashboard Recent/Deleted titles overlapped by scrolling content (by @rockchris099) [Github #8577](https://github.com/penpot/penpot/issues/8577)
- Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628)
- Fix hyphens stripped from export filenames (by @jamesrayammons) [Github #8901](https://github.com/penpot/penpot/issues/8901)
- Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959)
- Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960)
- Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984)
- Fix non-functional clear icon in change email modal inputs (by @Dexterity104) [Github #8977](https://github.com/penpot/penpot/issues/8977)
- Disable save button after saving account profile settings (by @Dexterity104) [Github #8979](https://github.com/penpot/penpot/issues/8979)
- Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990)
- Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067)
- Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516)
- Fix Settings and Notifications "Update Settings" button enabled state when form has no changes (by @moorsecopers99) [Github #9090](https://github.com/penpot/penpot/issues/9090)
- Fix "Help & Learning" submenu vertical alignment in account menu (by @juan-flores077) [Github #9137](https://github.com/penpot/penpot/issues/9137)
- Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` [Github #8409](https://github.com/penpot/penpot/issues/8409)
- Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479)
- Fix colorpicker layout so the eyedropper button is visible again [Taiga #14057](https://tree.taiga.io/project/penpot/issue/14057)
- Fix Plugin API variant creation failing due to undocumented multi-step workflow [#10075](https://github.com/penpot/penpot/issues/10075) (PR: [#10149](https://github.com/penpot/penpot/pull/10149))
- Fix workspace crash when editing text shapes with degenerate selrect [#10617](https://github.com/penpot/penpot/issues/10617) (PR: [#10618](https://github.com/penpot/penpot/pull/10618))
- Fix SVG stroke line join not applied when pasting strokes [#4836](https://github.com/penpot/penpot/issues/4836) (PR: [#9982](https://github.com/penpot/penpot/pull/9982), [#10019](https://github.com/penpot/penpot/pull/10019))
- Fix blend-mode hover preview on canvas not reverted when dismissing dropdown (by @jack-stormentswe) [#9235](https://github.com/penpot/penpot/issues/9235) (PR: [#9237](https://github.com/penpot/penpot/pull/9237))
- Fix blend-mode hover preview on canvas not reverted when dismissing dropdown (by @davidv399) [#9235](https://github.com/penpot/penpot/issues/9235) (PR: [#9237](https://github.com/penpot/penpot/pull/9237))
- Fix View Mode mouse-leave and click in combination not working [#4855](https://github.com/penpot/penpot/issues/4855) (PR: [#9991](https://github.com/penpot/penpot/pull/9991))
- Fix Storybook UI missing scrollbar (by @MilosM348) [#6049](https://github.com/penpot/penpot/issues/6049) (PR: [#9319](https://github.com/penpot/penpot/pull/9319))
- Fix font selector missing intermediate font weights for Source Sans Pro and similar fonts (by @dhgoal) [#7378](https://github.com/penpot/penpot/issues/7378) (PR: [#9247](https://github.com/penpot/penpot/pull/9247))
- Fix plugin API `typography.remove()` passing wrong parameter format (by @leonaIee) [#8223](https://github.com/penpot/penpot/issues/8223) (PR: [#9279](https://github.com/penpot/penpot/pull/9279))
- Fix plugin API `typography.remove()` passing wrong parameter format (by @peter-rango) [#8223](https://github.com/penpot/penpot/issues/8223) (PR: [#9279](https://github.com/penpot/penpot/pull/9279))
- Fix plugin API fills and strokes array elements being read-only (by @RenzoMXD) [#8357](https://github.com/penpot/penpot/issues/8357) (PR: [#9161](https://github.com/penpot/penpot/pull/9161))
- Fix "Show Guides" shortcut not working on German keyboards (by @RenzoMXD) [#8423](https://github.com/penpot/penpot/issues/8423) (PR: [#9209](https://github.com/penpot/penpot/pull/9209))
- Fix token validation failing when a malformed token exists in the Component category [#9010](https://github.com/penpot/penpot/issues/9010) (PR: [#9025](https://github.com/penpot/penpot/pull/9025), [#9825](https://github.com/penpot/penpot/pull/9825))
- Fix prototype interaction targets appearing in View Mode automatically when library component changes (by @jeffrey701) [#9049](https://github.com/penpot/penpot/issues/9049) (PR: [#9695](https://github.com/penpot/penpot/pull/9695))
- Fix Docker frontend image missing CSS reference (by @NativeTeachingAidsB) [#9135](https://github.com/penpot/penpot/issues/9135) (PR: [#9840](https://github.com/penpot/penpot/pull/9840))
- Fix MCP media upload error and SVG data URI image parsing (by @claytonlin1110) [#9164](https://github.com/penpot/penpot/issues/9164) (PR: [#9201](https://github.com/penpot/penpot/pull/9201))
- Fix lost-update race on team features during concurrent file creation (by @JPette1783) [#9197](https://github.com/penpot/penpot/issues/9197) (PR: [#9198](https://github.com/penpot/penpot/pull/9198))
- Fix get-profile RPC method silently masking DB errors as "Anonymous User" (by @jack-stormentswe) [#9253](https://github.com/penpot/penpot/issues/9253) (PR: [#9254](https://github.com/penpot/penpot/pull/9254))
- Fix lost-update race on team features during concurrent file creation (by @Lobster-0429) [#9197](https://github.com/penpot/penpot/issues/9197) (PR: [#9198](https://github.com/penpot/penpot/pull/9198))
- Fix get-profile RPC method silently masking DB errors as "Anonymous User" (by @davidv399) [#9253](https://github.com/penpot/penpot/issues/9253) (PR: [#9254](https://github.com/penpot/penpot/pull/9254))
- Fix crash when creating or editing tokens named "white" or "black" [#9256](https://github.com/penpot/penpot/issues/9256) (PR: [#9034](https://github.com/penpot/penpot/pull/9034))
- Fix conditional use-ctx hook violation in shape-wrapper (by @Dexterity104) [#9280](https://github.com/penpot/penpot/issues/9280) (PR: [#9281](https://github.com/penpot/penpot/pull/9281))
- Make ShapeImageIds byte conversion fallible to prevent panics (by @Dexterity104) [#9282](https://github.com/penpot/penpot/issues/9282) (PR: [#9283](https://github.com/penpot/penpot/pull/9283))
- Prevent viewers from overwriting file thumbnails (by @jony376) [#9284](https://github.com/penpot/penpot/issues/9284) (PR: [#9285](https://github.com/penpot/penpot/pull/9285))
- Fix plugin API showing incorrect error messages for invalid operations (by @bitcompass) [#9417](https://github.com/penpot/penpot/issues/9417) (PR: [#9486](https://github.com/penpot/penpot/pull/9486))
- Add inactivity timeout to SSE sessions to match Streamable HTTP sessions [#9432](https://github.com/penpot/penpot/issues/9432) (PR: [#9464](https://github.com/penpot/penpot/pull/9464))
- Fix component variant switching behaving differently on two identical copies (by @MischaPanch) [#9498](https://github.com/penpot/penpot/issues/9498) (PR: [#9434](https://github.com/penpot/penpot/pull/9434))
- Populate is-indirect flag on file libraries from relation graph (by @Dexterity104) [#9506](https://github.com/penpot/penpot/issues/9506) (PR: [#9289](https://github.com/penpot/penpot/pull/9289))
- Add missing error message for invalid shadow token [#9583](https://github.com/penpot/penpot/issues/9583) (PR: [#9809](https://github.com/penpot/penpot/pull/9809))
- Fix moving a component in a library triggering stale update notification in dependent files [#9629](https://github.com/penpot/penpot/issues/9629) (PR: [#9616](https://github.com/penpot/penpot/pull/9616))
- Fix newly created token not visible when placed above existing tokens in the tree [#9711](https://github.com/penpot/penpot/issues/9711) (PR: [#9803](https://github.com/penpot/penpot/pull/9803))
@@ -111,8 +115,101 @@
- Fix missing three dots button for column and row edit menu in WebKit/Safari [#9993](https://github.com/penpot/penpot/issues/9993) (PR: [#9994](https://github.com/penpot/penpot/pull/9994))
- Fix exported path with strokes being cut off in SVG file [#9995](https://github.com/penpot/penpot/issues/9995) (PR: [#9996](https://github.com/penpot/penpot/pull/9996))
- Fix French Canada locale falling back to French translations instead of French Canadian (by @alexismo) [#10017](https://github.com/penpot/penpot/issues/10017) (PR: [#10027](https://github.com/penpot/penpot/pull/10027))
- Fix inconsistent float precision in typography lineHeight API responses [#3658](https://github.com/penpot/penpot/issues/3658) (PR: [#9973](https://github.com/penpot/penpot/pull/9973))
- Fix variant switching preserving stale absolute positions in nested instance overrides [#9749](https://github.com/penpot/penpot/issues/9749) (PR: [#9691](https://github.com/penpot/penpot/pull/9691))
- Fix missing top border on first element in shared libraries list [#9910](https://github.com/penpot/penpot/issues/9910) (PR: [#10062](https://github.com/penpot/penpot/pull/10062))
- Fix theme not applied correctly in sample plugins [#9957](https://github.com/penpot/penpot/issues/9957) (PR: [#9955](https://github.com/penpot/penpot/pull/9955))
- Fix export presets not being saved in view mode inspect panel [#9971](https://github.com/penpot/penpot/issues/9971) (PR: [#9972](https://github.com/penpot/penpot/pull/9972))
- Fix cropped outer stroke of rotated board in view mode [#9978](https://github.com/penpot/penpot/issues/9978) (PR: [#9979](https://github.com/penpot/penpot/pull/9979))
- Fix re-registration with same email showing verification prompt when email verification is disabled [#9998](https://github.com/penpot/penpot/issues/9998) (PR: [#9999](https://github.com/penpot/penpot/pull/9999))
- Fix toggle color library visibility from color picker shortcut button [#10036](https://github.com/penpot/penpot/issues/10036) (PR: [#10129](https://github.com/penpot/penpot/pull/10129))
- Improve image rendering performance by making snapshots async [#10045](https://github.com/penpot/penpot/issues/10045) (PR: [#10150](https://github.com/penpot/penpot/pull/10150))
- Fix text layers wrapping to two lines after copy-pasting between files until edited [#10052](https://github.com/penpot/penpot/issues/10052) (PR: [#10081](https://github.com/penpot/penpot/pull/10081))
- Fix plugin API creating shapes on wrong page and cross-page appendChild silently no-ops [#10078](https://github.com/penpot/penpot/issues/10078) (PR: [#10085](https://github.com/penpot/penpot/pull/10085))
- Fix token dropdown triggered with select arrow not replacing previous content [#10104](https://github.com/penpot/penpot/issues/10104) (PR: [#10174](https://github.com/penpot/penpot/pull/10174))
- Fix incorrect color count in color libraries dropdown (by @Krishcode264) [#10120](https://github.com/penpot/penpot/issues/10120) (PR: [#10281](https://github.com/penpot/penpot/pull/10281))
- Fix color picker eyedropper returning wrong color values on some GPUs [#10135](https://github.com/penpot/penpot/issues/10135) (PR: [#10255](https://github.com/penpot/penpot/pull/10255))
- Fix content from previous rendered frame visible in rounded frame transparent areas in viewer WebGL [#10161](https://github.com/penpot/penpot/issues/10161) (PR: [#10191](https://github.com/penpot/penpot/pull/10191))
- Fix exported HTML/CSS not correctly rendered [#10214](https://github.com/penpot/penpot/issues/10214) (PR: [#10217](https://github.com/penpot/penpot/pull/10217))
- Fix template import failing from dashboard when downloading example files (by @mdbenito) [#10317](https://github.com/penpot/penpot/issues/10317) (PR: [#10308](https://github.com/penpot/penpot/pull/10308))
- Emit grid layout telemetry when adding via MCP or plugins [#10307](https://github.com/penpot/penpot/issues/10307) (PR: [#10319](https://github.com/penpot/penpot/pull/10319))
- Fix guide hover feedback sticking or stopping after interaction [#10321](https://github.com/penpot/penpot/issues/10321) (PR: [#10333](https://github.com/penpot/penpot/pull/10333))
- Fix rulers visible in thumbnail rendering [#10328](https://github.com/penpot/penpot/issues/10328)
- Fix page switch blur disappearing early [#10332](https://github.com/penpot/penpot/issues/10332) (PR: [#10337](https://github.com/penpot/penpot/pull/10337))
- Fix open overlay board mispositioned with repeated image artifacts in viewer WebGL [#10180](https://github.com/penpot/penpot/issues/10180) (PR: [#10191](https://github.com/penpot/penpot/pull/10191))
- Fix color picker preview when browser has non-100% zoom level [#10265](https://github.com/penpot/penpot/issues/10265) (PR: [#10305](https://github.com/penpot/penpot/pull/10305))
- Fix negative margins no longer working [#10001](https://github.com/penpot/penpot/issues/10001) (PR: [#10353](https://github.com/penpot/penpot/pull/10353))
- Fix undo in properties panel reversing each drag step instead of the entire drag when adjusting numeric inputs [#10066](https://github.com/penpot/penpot/issues/10066) (PR: [#10193](https://github.com/penpot/penpot/pull/10193))
- Fix Plugin API reference token creation and token application failing when the target token set is inactive [#10070](https://github.com/penpot/penpot/issues/10070), [#10071](https://github.com/penpot/penpot/issues/10071) (PR: [#10297](https://github.com/penpot/penpot/pull/10297))
- Fix Plugin API rejecting numeric token values with a generic validation error [#10073](https://github.com/penpot/penpot/issues/10073) (PR: [#10270](https://github.com/penpot/penpot/pull/10270))
- Fix Plugin API whole-page export failing with a generic HTTP error [#10076](https://github.com/penpot/penpot/issues/10076) (PR: [#10148](https://github.com/penpot/penpot/pull/10148))
- Fix Plugin API applyToken failing on padding properties [#10077](https://github.com/penpot/penpot/issues/10077) (PR: [#10087](https://github.com/penpot/penpot/pull/10087))
- Fix Plugin API crash when instancing or detaching variants [#10099](https://github.com/penpot/penpot/issues/10099) (PR: [#10140](https://github.com/penpot/penpot/pull/10140))
- Fix black square appearing on first view mode launch [#10188](https://github.com/penpot/penpot/issues/10188) (PR: [#10210](https://github.com/penpot/penpot/pull/10210))
- Fix Plugin API openPage() not switching active page within the same execution [#10195](https://github.com/penpot/penpot/issues/10195) (PR: [#10085](https://github.com/penpot/penpot/pull/10085))
- Fix Plugin API appending shapes to the active page instead of the specified .root [#10196](https://github.com/penpot/penpot/issues/10196) (PR: [#10271](https://github.com/penpot/penpot/pull/10271))
- Fix Plugin API createPage() not making the new page active synchronously [#10197](https://github.com/penpot/penpot/issues/10197) (PR: [#10085](https://github.com/penpot/penpot/pull/10085))
- Fix Plugin API Flow.startingBoard returning null after createFlow [#10203](https://github.com/penpot/penpot/issues/10203) (PR: [#10244](https://github.com/penpot/penpot/pull/10244))
- Fix Plugin API page.flows returning null instead of empty array when page has no flows [#10204](https://github.com/penpot/penpot/issues/10204) (PR: [#10246](https://github.com/penpot/penpot/pull/10246))
- Fix Plugin API auto-creating a stray 'Flow 1' when wiring interactions [#10205](https://github.com/penpot/penpot/issues/10205) (PR: [#10231](https://github.com/penpot/penpot/pull/10231))
- Fix Plugin API createText throwing error when called with an empty string [#10206](https://github.com/penpot/penpot/issues/10206) (PR: [#10219](https://github.com/penpot/penpot/pull/10219))
- Fix export rendering auto-width text with a substituted fallback font [#10208](https://github.com/penpot/penpot/issues/10208) (PR: [#10238](https://github.com/penpot/penpot/pull/10238))
- Expose 'Fix when scrolling' (sticky) constraint in the Plugin API [#10209](https://github.com/penpot/penpot/issues/10209) (PR: [#10218](https://github.com/penpot/penpot/pull/10218))
- Fix long typography token name in remap modal [#10301](https://github.com/penpot/penpot/issues/10301) (PR: [#10356](https://github.com/penpot/penpot/pull/10356))
- Fix 'Go to your Penpot' link on error page redirecting to the same team instead of user home [#10324](https://github.com/penpot/penpot/issues/10324) (PR: [#10322](https://github.com/penpot/penpot/pull/10322))
- Fix premature WASM view-interaction end during pan/zoom pause [#10424](https://github.com/penpot/penpot/issues/10424) (PR: [#10425](https://github.com/penpot/penpot/pull/10425))
- Fix emojis without fill but with outer stroke not being rendered [#10473](https://github.com/penpot/penpot/issues/10473) (PR: [#10519](https://github.com/penpot/penpot/pull/10519))
- Fix texts with lots of emojis not being rendered across multiple tiles [#10474](https://github.com/penpot/penpot/issues/10474) (PR: [#10504](https://github.com/penpot/penpot/pull/10504))
- Fix artifacts in texts with inner strokes [#10476](https://github.com/penpot/penpot/issues/10476) (PR: [#10509](https://github.com/penpot/penpot/pull/10509))
- Fix open overlay position control showing only center icon instead of full grid [#10176](https://github.com/penpot/penpot/issues/10176) (PR: [#10512](https://github.com/penpot/penpot/pull/10512))
- Fix wrong text color in selection size badge [#10256](https://github.com/penpot/penpot/issues/10256) (PR: [#10393](https://github.com/penpot/penpot/pull/10393))
- Fix several issues with plugins [#10394](https://github.com/penpot/penpot/issues/10394)
- Fix pixel grid rendered on top of the rulers [#10426](https://github.com/penpot/penpot/issues/10426) (PR: [#10430](https://github.com/penpot/penpot/pull/10430))
- Fix double-clicking text without fills creating a black file [#10472](https://github.com/penpot/penpot/issues/10472) (PR: [#10483](https://github.com/penpot/penpot/pull/10483))
- Fix SVG raw caching prevented by unnecessary Cow wrapping [#10488](https://github.com/penpot/penpot/issues/10488) (PR: [#10492](https://github.com/penpot/penpot/pull/10492))
- Add component reset operation to plugin API [#10561](https://github.com/penpot/penpot/issues/10561) (PR: [#10533](https://github.com/penpot/penpot/pull/10533))
- Fix blur menu alignment in Firefox [#10576](https://github.com/penpot/penpot/issues/10576) (PR: [#10575](https://github.com/penpot/penpot/pull/10575))
- Fix sidebar not showing all elements with grid layout [#10539](https://github.com/penpot/penpot/issues/10539) (PR: [#10600](https://github.com/penpot/penpot/pull/10600))
- Fix sidebar getting stuck when selecting shapes that haven't loaded yet [#10599](https://github.com/penpot/penpot/issues/10599) (PR: [#10600](https://github.com/penpot/penpot/pull/10600))
- Fix text shape bounding boxes not updating after remote fonts finish loading [#10585](https://github.com/penpot/penpot/issues/10585) (PR: [#10566](https://github.com/penpot/penpot/pull/10566))
- Fix text editor crash from Draft.js selection offset exceeding DOM node length [#10607](https://github.com/penpot/penpot/issues/10607) (PR: [#10608](https://github.com/penpot/penpot/pull/10608))
- Fix workspace crash when converting SVG-raw shape to path [#10612](https://github.com/penpot/penpot/issues/10612) (PR: [#10613](https://github.com/penpot/penpot/pull/10613))
- Fix component variant panel crash when selecting multiple copies with mismatched property counts [#10615](https://github.com/penpot/penpot/issues/10615) (PR: [#10616](https://github.com/penpot/penpot/pull/10616))
- Fix workspace crash from recursion when clicking shape in comments mode [#10620](https://github.com/penpot/penpot/issues/10620) (PR: [#10622](https://github.com/penpot/penpot/pull/10622))
- Fix Plugin API validation error when listing shared plugin data keys [#10628](https://github.com/penpot/penpot/issues/10628) (PR: [#10632](https://github.com/penpot/penpot/pull/10632))
- Fix Plugin API silently dropping plugin data written to shared library [#10629](https://github.com/penpot/penpot/issues/10629) (PR: [#10632](https://github.com/penpot/penpot/pull/10632))
- Fix workspace crash when event target is a DOM text node [#10640](https://github.com/penpot/penpot/issues/10640) (PR: [#10641](https://github.com/penpot/penpot/pull/10641))
- Fix text shape position-data to include required fills in WASM and DOM calculation paths [#10646](https://github.com/penpot/penpot/issues/10646) (PR: [#10650](https://github.com/penpot/penpot/pull/10650))
- Log expired OIDC tokens as auth failures instead of server errors [#10635](https://github.com/penpot/penpot/issues/10635) (PR: [#10636](https://github.com/penpot/penpot/pull/10636))
- Return 400 instead of 500 when ImageMagick rejects invalid uploaded images [#10642](https://github.com/penpot/penpot/issues/10642) (PR: [#10643](https://github.com/penpot/penpot/pull/10643))
## 2.16.0 (Unreleased)
## 2.16.2
### :bug: Bugs fixed
- Fix error 500 when submitting the contact form [#10178](https://github.com/penpot/penpot/issues/10178) (PR: [#10419](https://github.com/penpot/penpot/pull/10419))
- Fix text editor modifying content and detaching applied typography tokens [#10389](https://github.com/penpot/penpot/issues/10389) (PR: [#10402](https://github.com/penpot/penpot/pull/10402))
## 2.16.1
### :sparkles: New features & Enhancements
- Batch multiple thumbnail deletions into a single RPC call [#9944](https://github.com/penpot/penpot/issues/9944) (PR: [#9943](https://github.com/penpot/penpot/pull/9943))
- Add WebSocket proxy configuration for MCP in Nginx example (by @lancatlin) [#10153](https://github.com/penpot/penpot/issues/10153) (PR: [#10152](https://github.com/penpot/penpot/pull/10152))
- Add tenant prefix to MCP Redis channel names for multi-environment isolation [#10277](https://github.com/penpot/penpot/issues/10277) (PR: [#10276](https://github.com/penpot/penpot/pull/10276))
- Show MCP key on Integrations page and remove non-recoverable warning from modal [#10290](https://github.com/penpot/penpot/issues/10290) (PR: [#10298](https://github.com/penpot/penpot/pull/10298))
- Add MCP status button with single-tab connection control [#9923](https://github.com/penpot/penpot/issues/9923) (PR: [#9930](https://github.com/penpot/penpot/pull/9930))
### :bug: Bugs fixed
- Fix race condition between MCP initialization and plugin runtime [#10138](https://github.com/penpot/penpot/issues/10138) (PR: [#10137](https://github.com/penpot/penpot/pull/10137))
- Filter ignorable React removeChild errors from browser extensions in error boundary [#10146](https://github.com/penpot/penpot/issues/10146) (PR: [#10145](https://github.com/penpot/penpot/pull/10145))
- Show resolved values in font family token combobox when pasting comma-separated values [#10212](https://github.com/penpot/penpot/issues/10212) (PR: [#10215](https://github.com/penpot/penpot/pull/10215))
- Fix MCP server status toggle persistence and missing workspace connection options [#10292](https://github.com/penpot/penpot/issues/10292) (PR: [#10226](https://github.com/penpot/penpot/pull/10226))
- Allow pasting comma-separated emails in the invite members modal [#10173](https://github.com/penpot/penpot/issues/10173) (PR: [#10186](https://github.com/penpot/penpot/pull/10186))
- Fix text element edit detaching applied color tokens [#9255](https://github.com/penpot/penpot/issues/9255) (PR: [#9525](https://github.com/penpot/penpot/pull/9525), [#9814](https://github.com/penpot/penpot/pull/9814), [#10340](https://github.com/penpot/penpot/pull/10340))
## 2.16.0
### :boom: Breaking changes & Deprecations
@@ -138,7 +235,6 @@
- Duplicate token group [#9638](https://github.com/penpot/penpot/issues/9638) (PR: [#8886](https://github.com/penpot/penpot/pull/8886))
- Copy token name from contextual menu [#9639](https://github.com/penpot/penpot/issues/9639) (PR: [#8566](https://github.com/penpot/penpot/pull/8566))
- Add drag-to-change for numeric inputs in workspace sidebar (by @RenzoMXD) [#2466](https://github.com/penpot/penpot/issues/2466) (PR: [#8536](https://github.com/penpot/penpot/pull/8536))
- Add CSS linter [#9636](https://github.com/penpot/penpot/issues/9636) (PR: [#8592](https://github.com/penpot/penpot/pull/8592))
- Add per-group add button for typographies (by @eureka0928) [#5275](https://github.com/penpot/penpot/issues/5275) (PR: [#8895](https://github.com/penpot/penpot/pull/8895))
- Add Find & Replace for text content and layer names (by @statxc) [#7108](https://github.com/penpot/penpot/issues/7108) (PR: [#8899](https://github.com/penpot/penpot/pull/8899), [#9687](https://github.com/penpot/penpot/pull/9687))
- Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [#8773](https://github.com/penpot/penpot/issues/8773) (PR: [#8874](https://github.com/penpot/penpot/pull/8874))
@@ -169,10 +265,9 @@
- Preserve Inkscape labels when pasting SVGs (by @jeffrey701) [#7869](https://github.com/penpot/penpot/issues/7869) (PR: [#9252](https://github.com/penpot/penpot/pull/9252))
- Add Alt+click to expand layer subtree (by @MilosM348) [#7736](https://github.com/penpot/penpot/issues/7736) (PR: [#9179](https://github.com/penpot/penpot/pull/9179))
- Allow deleting the profile avatar after uploading (by @moorsecopers99) [#9067](https://github.com/penpot/penpot/issues/9067) (PR: [#9068](https://github.com/penpot/penpot/pull/9068))
- Clarify self-hosted OIDC configuration for containerized (by @sancfc) [#9764](https://github.com/penpot/penpot/issues/9764) (PR: [#9758](https://github.com/penpot/penpot/pull/9758))
- Update User Guide with 2.16 features (by @myfunnyandy) [#9767](https://github.com/penpot/penpot/issues/9767) (PR: [#9768](https://github.com/penpot/penpot/pull/9768))
- Improve file validation performance and fix orphan shape detection [#9790](https://github.com/penpot/penpot/issues/9790) (PR: [#9789](https://github.com/penpot/penpot/pull/9789))
- Add v2.16 release notes (What's new modal) [#9945](https://github.com/penpot/penpot/issues/9945) (PR: [#9940](https://github.com/penpot/penpot/pull/9940))
- Enable multi-instance horizontal scaling for MCP server [#10000](https://github.com/penpot/penpot/issues/10000) (PR: [#10013](https://github.com/penpot/penpot/pull/10013))
### :bug: Bugs fixed
@@ -234,7 +329,6 @@
- Fix `:heigth` typo in clipboard frame-same-size? (by @iot2edge) [#9249](https://github.com/penpot/penpot/issues/9249) (PR: [#9250](https://github.com/penpot/penpot/pull/9250))
- Fix Settings Update button enabled state (by @moorsecopers99) [#9090](https://github.com/penpot/penpot/issues/9090) (PR: [#9091](https://github.com/penpot/penpot/pull/9091))
- Fix library updates reappearing after reload [#9326](https://github.com/penpot/penpot/issues/9326) (PR: [#9563](https://github.com/penpot/penpot/pull/9563))
- Fix dependency libraries visible after unlinking main library [#9331](https://github.com/penpot/penpot/issues/9331) (PR: [#9511](https://github.com/penpot/penpot/pull/9511))
- Fix internal error on margins [#9309](https://github.com/penpot/penpot/issues/9309) (PR: [#9311](https://github.com/penpot/penpot/pull/9311))
- Remove drag-to-change when token applied on numeric input [#9313](https://github.com/penpot/penpot/issues/9313) (PR: [#9314](https://github.com/penpot/penpot/pull/9314))
- Fix extra input on canvas background [#9359](https://github.com/penpot/penpot/issues/9359) (PR: [#9360](https://github.com/penpot/penpot/pull/9360))
@@ -242,20 +336,18 @@
- Fix several color picker issues [#9556](https://github.com/penpot/penpot/issues/9556) (PR: [#9558](https://github.com/penpot/penpot/pull/9558))
- Fix asset icon broken on Asset tab [#9587](https://github.com/penpot/penpot/issues/9587) (PR: [#9612](https://github.com/penpot/penpot/pull/9612))
- Fix text fill color stops updating in multiselect with texts [#9608](https://github.com/penpot/penpot/issues/9608) (PR: [#9549](https://github.com/penpot/penpot/pull/9549))
- Fix editing a legacy text element silently detaches its color token [#9255](https://github.com/penpot/penpot/issues/9255) (PR: [#9525](https://github.com/penpot/penpot/pull/9525))
- Fix token application to grid paddings [#9494](https://github.com/penpot/penpot/issues/9494) (PR: [#9630](https://github.com/penpot/penpot/pull/9630))
- Fix file crashing when switching a variant [#9259](https://github.com/penpot/penpot/issues/9259) (PR: [#9147](https://github.com/penpot/penpot/pull/9147))
- Fix set activation after renaming [#9329](https://github.com/penpot/penpot/issues/9329) (PR: [#9545](https://github.com/penpot/penpot/pull/9545))
- Fix font selection position hiding available fonts [#9489](https://github.com/penpot/penpot/issues/9489) (PR: [#9499](https://github.com/penpot/penpot/pull/9499))
- Fix numeric input changes not saved when clicking on viewport [#9491](https://github.com/penpot/penpot/issues/9491) (PR: [#9548](https://github.com/penpot/penpot/pull/9548))
- Fix resize cursor appearing on login and register buttons [#9505](https://github.com/penpot/penpot/issues/9505) (PR: [#9590](https://github.com/penpot/penpot/pull/9590))
- Fix version restore restoring first previewed version instead of selected one [#9588](https://github.com/penpot/penpot/issues/9588) (PR: [#9626](https://github.com/penpot/penpot/pull/9626))
- Fix incorrect error message when applying tokens while editing text [#9620](https://github.com/penpot/penpot/issues/9620) (PR: [#9708](https://github.com/penpot/penpot/pull/9708))
- Fix standalone tokens ordering separated from token groups [#9733](https://github.com/penpot/penpot/issues/9733) (PR: [#9736](https://github.com/penpot/penpot/pull/9736))
- Fix delete invitation modal readability in light theme [#9737](https://github.com/penpot/penpot/issues/9737) (PR: [#9747](https://github.com/penpot/penpot/pull/9747))
- Fix team invitation not automatically accepted after account validation [#9776](https://github.com/penpot/penpot/issues/9776) (PR: [#9782](https://github.com/penpot/penpot/pull/9782))
- Fix design tokens vanishing from the sidebar when a token name collides with a token-group prefix from another active set (e.g. `a` in one set and `a.b` in another); the colliding token is now kept and rendered as a broken pill [Github #9584](https://github.com/penpot/penpot/issues/9584)
- Fix Plugin API addRulerGuide creating guides on page instead of board (by @girafic) [#8225](https://github.com/penpot/penpot/issues/8225) (PR: [#8632](https://github.com/penpot/penpot/pull/8632))
- Fix text editor not swapping correctly when enabling/disabling WebGL [#10015](https://github.com/penpot/penpot/issues/10015)
- Fix WebGL renderer focus mode leaving artefacts [#10061](https://github.com/penpot/penpot/issues/10061) (PR: [#10091](https://github.com/penpot/penpot/pull/10091))
- Fix double click on text selecting underlying element when WebGL render is enabled [#10080](https://github.com/penpot/penpot/issues/10080) (PR: [#10123](https://github.com/penpot/penpot/pull/10123))
- Fix publishing or unpublishing file as library failing with unexpected state found error [#10094](https://github.com/penpot/penpot/issues/10094) (PR: [#10093](https://github.com/penpot/penpot/pull/10093))
- Fix team invitation failing when email address contains consecutive dots in domain [#10097](https://github.com/penpot/penpot/issues/10097) (PR: [#10096](https://github.com/penpot/penpot/pull/10096))
- Add detailed error messages for unspecified import errors [#9759](https://github.com/penpot/penpot/issues/9759) (PR: [#9886](https://github.com/penpot/penpot/pull/9886))
- Fix mask with children not applying blur correctly [#10004](https://github.com/penpot/penpot/issues/10004) (PR: [#10028](https://github.com/penpot/penpot/pull/10028))
- Fix WebGL italic font mismatch when custom font variants are mapped [#10060](https://github.com/penpot/penpot/issues/10060) (PR: [#10122](https://github.com/penpot/penpot/pull/10122))
## 2.15.4
+41 -97
View File
@@ -14,9 +14,9 @@ Center](https://help.penpot.app/).
- [Reporting Bugs](#reporting-bugs)
- [Pull Requests](#pull-requests)
- [Workflow](#workflow)
- [Title format](#title-format)
- [Description](#description)
- [Branch naming](#branch-naming)
- [Format](#format)
- [Title format](#title-format)
- [Description](#description)
- [Review process](#review-process)
- [What we won't accept](#what-we-wont-accept)
- [Good first issues](#good-first-issues)
@@ -73,35 +73,23 @@ Advisories](https://github.com/penpot/penpot/security/advisories)
4. **Format and lint** — run the checks described in
[Formatting and Linting](#formatting-and-linting) before submitting.
### Title format
### Format
#### Title
> **IMPORTANT:** When a PR is squash-merged, the PR title becomes the
> commit message on the main branch. Getting the title right matters.
Pull request titles **must** follow the same convention as commit subjects:
```
:emoji: <subject>
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
```
- Use the **imperative mood** (e.g. "Fix", not "Fixed").
- Capitalize the first letter of the subject.
- Do not end the subject with a period.
- Keep the subject to **70 characters** or fewer.
- Use one of the [commit type emojis](#commit-types) listed below.
Read [Creating Commits](./.serena/memories/workflow/creating-commits.md)
for more concrete information.
When a PR contains multiple unrelated commits, choose the emoji that
best represents the dominant change.
**Examples:**
```
:bug: Fix unexpected error on launching modal
:sparkles: Enable new modal for profile
:zap: Improve performance of dashboard navigation
```
> **Note:** When a PR is squash-merged, the PR title becomes the
> commit message on the main branch. Getting the title right matters.
### Description
#### Description
Every pull request should include a description that helps reviewers
understand the change quickly:
@@ -114,24 +102,8 @@ understand the change quickly:
5. **Breaking changes** — call out anything that affects existing users
or requires migration steps.
### Branch naming
Use a descriptive branch name that reflects the type and scope of the
change:
```
<type>/<short-description>
```
Types: `fix`, `feat`, `refactor`, `docs`, `chore`, `perf`.
Optionally include the issue number:
```
fix/9122-email-blacklisting
feat/export-webp
refactor/layout-sizing
```
Read [Creating PRs](./.serena/memories/workflow/creating-prs.md)
for more concrete information.
### Review process
@@ -151,10 +123,10 @@ refactor/layout-sizing
To save time on both sides, please avoid submitting PRs that:
- Introduce new dependencies without prior discussion.
- Change the build system or CI configuration without maintainer
approval.
- Mix unrelated changes in a single PR — keep PRs focused on one
concern.
- Change the build system or CI configuration without maintainer approval.
- Mix unrelated changes in a single PR — keep PRs focused on one concern.
- Submit AI-generated code without human review.
- Skip local syntax and formatting checks before submitting.
- Skip the [discussion step](#workflow) for non-bug-fix changes.
### Good first issues
@@ -175,26 +147,26 @@ Commit messages must follow this format:
### Commit types
| Emoji | Description |
| ---------------------- | -------------------------- |
| :bug: | Bug fix |
| :sparkles: | Improvement or enhancement |
| :tada: | New feature |
| :recycle: | Refactor |
| :lipstick: | Cosmetic changes |
| :ambulance: | Critical bug fix |
| :books: | Documentation |
| :construction: | Work in progress |
| :boom: | Breaking change |
| :wrench: | Configuration update |
| :zap: | Performance improvement |
| :whale: | Docker-related change |
| :paperclip: | Other non-relevant changes |
| :arrow_up: | Dependency update |
| :arrow_down: | Dependency downgrade |
| :fire: | Removal of code or files |
| :globe_with_meridians: | Add or update translations |
| :rocket: | Epic or highlight |
| Emoji | Code | Description |
| ---------------------- | ------------------------ | -------------------------- |
| :bug: | `:bug:` | Bug fix |
| :sparkles: | `:sparkles:` | Improvement or enhancement |
| :tada: | `:tada:` | New feature |
| :recycle: | `:recycle:` | Refactor |
| :lipstick: | `:lipstick:` | Cosmetic changes |
| :ambulance: | `:ambulance:` | Critical bug fix |
| :books: | `:books:` | Documentation |
| :construction: | `:construction:` | Work in progress |
| :boom: | `:boom:` | Breaking change |
| :wrench: | `:wrench:` | Configuration update |
| :zap: | `:zap:` | Performance improvement |
| :whale: | `:whale:` | Docker-related change |
| :paperclip: | `:paperclip:` | Other non-relevant changes |
| :arrow_up: | `:arrow_up:` | Dependency update |
| :arrow_down: | `:arrow_down:` | Dependency downgrade |
| :fire: | `:fire:` | Removal of code or files |
| :globe_with_meridians: | `:globe_with_meridians:` | Add or update translations |
| :rocket: | `:rocket:` | Epic or highlight |
### Rules
@@ -217,36 +189,8 @@ Commit messages must follow this format:
## Formatting and Linting
We use [cljfmt](https://github.com/weavejester/cljfmt) for formatting and
[clj-kondo](https://github.com/clj-kondo/clj-kondo) for linting.
```bash
# Check formatting (does not modify files)
./scripts/check-fmt
# Fix formatting (modifies files in place)
./scripts/fmt
# Lint
./scripts/lint
```
For frontend SCSS, we use `stylelint` for linting and
`Prettier` for formatting:
```bash
cd frontend
# Lint SCSS
pnpm run lint:scss (does not modify files)
# Fix SCSS formatting (modifies files in place)
pnpm run fmt:scss
```
Ideally, run these as git pre-commit hooks.
[Husky](https://typicode.github.io/husky/#/) is a convenient option for
setting this up.
Each module has its own linting and formatting commands — see the relevant one on the
[Serena Memories](./.serena/memories/)
## Changelog
+2
View File
@@ -7,6 +7,8 @@ list.
## Security
* Noob Researcher
* Kirubakaran N
* Alisher (@7megaumka7)
* Husnain Iqbal (CEO OF ALPHA INFERNO PVT LTD)
* [Shiraz Ali Khan](https://www.linkedin.com/in/shiraz-ali-khan-1ba508180/)
+21 -24
View File
@@ -4,9 +4,9 @@
:deps
{penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.12.5"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
org.clojure/tools.namespace {:mvn/version "1.5.1"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-4"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-12"}
io.prometheus/simpleclient {:mvn/version "0.16.0"}
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
@@ -17,7 +17,7 @@
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
io.lettuce/lettuce-core {:mvn/version "7.5.1.RELEASE"}
io.lettuce/lettuce-core {:mvn/version "7.6.0.RELEASE"}
;; Minimal dependencies required by lettuce, we need to include them
;; explicitly because clojure dependency management does not support
;; yet the BOM format.
@@ -25,7 +25,7 @@
io.micrometer/micrometer-observation {:mvn/version "1.14.2"}
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
com.google.guava/guava {:mvn/version "33.4.8-jre"}
com.google.guava/guava {:mvn/version "33.6.0-jre"}
funcool/yetti
{:git/tag "v11.10"
@@ -34,47 +34,46 @@
:exclusions [org.slf4j/slf4j-api]}
com.github.seancorfield/next.jdbc
{:mvn/version "1.3.1093"}
{:mvn/version "1.3.1118"}
metosin/reitit-core {:mvn/version "0.9.1"}
metosin/reitit-core {:mvn/version "0.10.1"}
nrepl/nrepl {:mvn/version "1.7.0"}
org.postgresql/postgresql {:mvn/version "42.7.11"}
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
org.postgresql/postgresql {:mvn/version "42.7.13"}
org.xerial/sqlite-jdbc {:mvn/version "3.53.2.1"}
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
com.zaxxer/HikariCP {:mvn/version "7.1.0"}
io.whitfin/siphash {:mvn/version "2.0.0"}
buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.6.1-359"}
org.passay/passay {:mvn/version "1.6.6"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"}
org.jsoup/jsoup {:mvn/version "1.21.2"}
org.im4java/im4java
{:git/tag "1.4.0-penpot-2"
:git/sha "e2b3e16"
:git/url "https://github.com/penpot/im4java"}
org.jsoup/jsoup {:mvn/version "1.23.1"}
at.yawk.lz4/lz4-java
{:mvn/version "1.11.0"}
{:mvn/version "1.11.1"}
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
dawran6/emoji {:mvn/version "0.2.0"}
markdown-clj/markdown-clj {:mvn/version "1.12.4"}
markdown-clj/markdown-clj {:mvn/version "1.12.8"}
;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.44.4"}
software.amazon.awssdk/sts {:mvn/version "2.44.4"}}
software.amazon.awssdk/s3 {:mvn/version "2.50.1"}
software.amazon.awssdk/sts {:mvn/version "2.50.1"}}
:paths ["src" "resources" "target/classes"]
:aliases
{:dev
{:extra-deps
{com.bhauman/rebel-readline {:mvn/version "0.1.5"}
{:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
"--enable-native-access=ALL-UNNAMED"]
:extra-deps
{com.bhauman/rebel-readline {:mvn/version "0.1.11"}
clojure-humanize/clojure-humanize {:mvn/version "0.2.2"}
org.clojure/data.csv {:mvn/version "1.1.1"}
com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"}
@@ -83,14 +82,12 @@
:build
{:extra-deps
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
{io.github.clojure/tools.build {:mvn/version "0.10.14"}}
:ns-default build}
:test
{:main-opts ["-m" "kaocha.runner"]
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
"--sun-misc-unsafe-memory-access=allow"
"--enable-native-access=ALL-UNNAMED"]
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
:outdated
+3 -7
View File
@@ -104,24 +104,20 @@
[]
(try
(main/start)
:started
(catch Throwable cause
(ex/print-throwable cause))))
(defn- stop
[]
(main/stop)
:stopped)
(main/stop))
(defn restart
[]
(stop)
(repl/refresh :after 'user/start))
(main/restart))
(defn restart-all
[]
(stop)
(repl/refresh-all :after 'user/start))
(main/restart-all))
;; (defn compression-bench
;; [data]
+10 -9
View File
@@ -4,24 +4,25 @@
"license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL",
"private": true,
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
"packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
},
"dependencies": {
"luxon": "^3.4.4",
"sax": "^1.4.1"
"eventsource-parser": "^3.0.6",
"luxon": "^3.7.2",
"sax": "^1.6.1"
},
"devDependencies": {
"nodemon": "^3.1.2",
"nodemon": "^3.1.14",
"source-map-support": "^0.5.21",
"ws": "^8.17.0"
"ws": "^8.21.1"
},
"scripts": {
"lint": "clj-kondo --parallel --lint ../common/src src/",
"check-fmt": "cljfmt check --parallel=true src/ test/",
"fmt": "cljfmt fix --parallel=true src/ test/",
"test": "clojure -M:dev:test"
"lint:clj": "clj-kondo --config-dir ../.clj-kondo --lint ../common/src src/",
"check-fmt:clj": "cljfmt check --parallel=true src/ test/",
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
"test:e2e": "node --test --test-concurrency=1 test/e2e/*.test.mjs"
}
}
+51 -44
View File
@@ -8,22 +8,25 @@ importers:
.:
dependencies:
eventsource-parser:
specifier: ^3.0.6
version: 3.1.0
luxon:
specifier: ^3.4.4
specifier: ^3.7.2
version: 3.7.2
sax:
specifier: ^1.4.1
version: 1.4.3
specifier: ^1.6.1
version: 1.6.1
devDependencies:
nodemon:
specifier: ^3.1.2
version: 3.1.11
specifier: ^3.1.14
version: 3.1.14
source-map-support:
specifier: ^0.5.21
version: 0.5.21
ws:
specifier: ^8.17.0
version: 8.18.3
specifier: ^8.21.1
version: 8.21.1
packages:
@@ -31,15 +34,17 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
brace-expansion@5.0.9:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
engines: {node: 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -52,9 +57,6 @@ packages:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
@@ -64,6 +66,10 @@ packages:
supports-color:
optional: true
eventsource-parser@3.1.0:
resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==}
engines: {node: '>=18.0.0'}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -104,14 +110,15 @@ packages:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
engines: {node: '>=12'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nodemon@3.1.11:
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
nodemon@3.1.14:
resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==}
engines: {node: '>=10'}
hasBin: true
@@ -119,8 +126,8 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
picomatch@2.3.2:
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
engines: {node: '>=8.6'}
pstree.remy@1.1.8:
@@ -130,11 +137,12 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
sax@1.4.3:
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
sax@1.6.1:
resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
engines: {node: '>=11.0.0'}
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
semver@7.8.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
@@ -164,8 +172,8 @@ packages:
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
ws@8.21.1:
resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
@@ -181,16 +189,15 @@ snapshots:
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
picomatch: 2.3.2
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
binary-extensions@2.3.0: {}
brace-expansion@1.1.12:
brace-expansion@5.0.9:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
balanced-match: 4.0.4
braces@3.0.3:
dependencies:
@@ -210,14 +217,14 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
concat-map@0.0.1: {}
debug@4.4.3(supports-color@5.5.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 5.5.0
eventsource-parser@3.1.0: {}
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -247,20 +254,20 @@ snapshots:
luxon@3.7.2: {}
minimatch@3.1.2:
minimatch@10.2.5:
dependencies:
brace-expansion: 1.1.12
brace-expansion: 5.0.9
ms@2.1.3: {}
nodemon@3.1.11:
nodemon@3.1.14:
dependencies:
chokidar: 3.6.0
debug: 4.4.3(supports-color@5.5.0)
ignore-by-default: 1.0.1
minimatch: 3.1.2
minimatch: 10.2.5
pstree.remy: 1.1.8
semver: 7.7.3
semver: 7.8.5
simple-update-notifier: 2.0.0
supports-color: 5.5.0
touch: 3.1.1
@@ -268,21 +275,21 @@ snapshots:
normalize-path@3.0.0: {}
picomatch@2.3.1: {}
picomatch@2.3.2: {}
pstree.remy@1.1.8: {}
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
picomatch: 2.3.2
sax@1.4.3: {}
sax@1.6.1: {}
semver@7.7.3: {}
semver@7.8.5: {}
simple-update-notifier@2.0.0:
dependencies:
semver: 7.7.3
semver: 7.8.5
source-map-support@0.5.21:
dependencies:
@@ -303,4 +310,4 @@ snapshots:
undefsafe@2.0.5: {}
ws@8.18.3: {}
ws@8.21.1: {}
+2
View File
@@ -0,0 +1,2 @@
minimumReleaseAgeExclude:
- brace-expansion@5.0.8 || 5.0.9
@@ -122,7 +122,7 @@
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
@@ -254,4 +254,4 @@
</div>
</body>
</html>
</html>
@@ -122,7 +122,7 @@
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
@@ -122,7 +122,7 @@
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
@@ -122,7 +122,7 @@
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
@@ -29,10 +29,8 @@
style="vertical-align:top;" width="100%">
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
Penpot is the first Open Source design and prototyping platform meant for
cross-domain teams.
<div style="font-family:Source Sans Pro, sans-serif;font-size:14px;line-height:150%;text-align:center;color:#64666A;">
Penpot is the open-source design platform for teams that need scalable collaboration.
</div>
</td>
</tr>
@@ -100,9 +98,9 @@
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://penpot.app/" target="_blank">
<a href="https://www.reddit.com/r/Penpot" target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-uxbox.png"
src="{{ public-uri }}/images/email/logo-reddit.svg"
style="border-radius:3px;display:block;"
width="24" />
</a>
@@ -126,9 +124,9 @@
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://x.com/penpotapp" target="_blank">
<a href="https://www.linkedin.com/company/penpot" target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-x.png"
src="{{ public-uri }}/images/email/logo-linkedin.svg"
style="border-radius:3px;display:block;"
width="24" />
</a>
@@ -152,9 +150,9 @@
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://github.com/penpot/" target="_blank">
<a href="https://bsky.app/profile/penpot.app" target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-github.png"
src="{{ public-uri }}/images/email/logo-bluesky.svg"
style="border-radius:3px;display:block;"
width="24" />
</a>
@@ -178,10 +176,36 @@
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.linkedin.com/company/penpotdesign/"
<a href="https://github.com/penpot" target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-github.svg"
style="border-radius:3px;display:block;"
width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-radius:3px;width:24px;">
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://x.com/penpotapp"
target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-linkedin.png"
src="{{ public-uri }}/images/email/logo-x.svg"
style="border-radius:3px;display:block;"
width="24" />
</a>
@@ -205,9 +229,9 @@
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://fosstodon.org/@penpot/" target="_blank">
<a href="https://www.youtube.com/@Penpot" target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-mastodon.png"
src="{{ public-uri }}/images/email/logo-youtube.svg"
style="border-radius:3px;display:block;"
width="24" />
</a>
@@ -231,10 +255,36 @@
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://tree.taiga.io/project/penpot"
<a href="https://www.instagram.com/penpot.app"
target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-taiga.png"
src="{{ public-uri }}/images/email/logo-instagram.svg"
style="border-radius:3px;display:block;"
width="24" />
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!--[if mso | IE]>
</td>
<td>
<![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="float:none;display:inline-table;">
<tr>
<td style="padding:0 8px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-radius:3px;width:24px;">
<tr>
<td
style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://fosstodon.org/@penpot" target="_blank">
<img height="24"
src="{{ public-uri }}/images/email/logo-mastodon.svg"
style="border-radius:3px;display:block;"
width="24" />
</a>
@@ -320,4 +370,4 @@
</td>
</tr>
</table>
<![endif]-->
<![endif]-->
@@ -1,10 +0,0 @@
Hello!
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:25 }}”.
Accept invitation using this link:
{{ public-uri }}/#/auth/verify-token?token={{token}}
Enjoy!
The Penpot team.
@@ -122,7 +122,7 @@
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
@@ -195,21 +195,45 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20" style="display:inline-block;vertical-align:middle;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20"
style="display:inline-block;vertical-align:middle;">
<tr>
<td width="20" height="20" align="center" valign="middle"
background="{% if organization.logo %}{{organization.logo}}{% else %}{{organization.avatar-bg-url}}{% endif %}"
style="width:20px;height:20px;text-align:center;font-weight:bold;font-size:9px;line-height:20px;color:#ffffff;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius: 50%;color:black">
background="{% if organization.logo %}{{organization.logo}}{% else %}{{organization.avatar-bg-url}}{% endif %}"
style="width:20px;height:20px;text-align:center;font-weight:bold;font-size:9px;line-height:20px;color:#ffffff;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius: 50%;color:black">
{% if organization.initials %}{{organization.initials}}{% endif %}
</td>
</tr>
</table>
<span style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
{{ organization.name|abbreviate:50 }}
<span
style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
{{ organization.name|abbreviate:50 }}
</span>
</div>
</td>
</tr>
{% if organization.sso-active %}
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its
teams and files now goes through your organization's identity provider.
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
If you can't get in, your account probably isn't in the directory yet.
To get access, contact the organization owner.
</div>
</td>
</tr>
{% endif %}
<tr>
<td align="center" vertical-align="middle"
style="font-size:0px;padding:10px 25px;word-break:break-word;">
@@ -0,0 +1,17 @@
Hello!
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization.name|abbreviate:50 }}”.
{% if organization.sso-active %}
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files now goes
through your organization's identity provider.
If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
{% endif %}
Accept invitation using this link:
{{ public-uri }}/#/auth/verify-token?token={{token}}
Enjoy!
The Penpot team.
@@ -122,7 +122,7 @@
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
@@ -186,10 +186,31 @@
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”{% if organization %}
part of the organization “{{ organization|abbreviate:25 }}”{% endif %}.</div>
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:50 }}”{% if
organization %}
part of the organization “{{ organization.name|abbreviate:50 }}”{% endif %}.</div>
</td>
</tr>
{% if organization.sso-active %}
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to
its teams and files now goes through your organization's identity provider.
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
If you can't get in, your account probably isn't in the directory yet.
To get access, contact the organization owner.
</div>
</td>
</tr>
{% endif %}
<tr>
<td align="center" vertical-align="middle"
style="font-size:0px;padding:10px 25px;word-break:break-word;">
@@ -1,6 +1,13 @@
Hello!
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization|abbreviate:25 }}"{% endif %}.
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:50 }}"{% if organization %}, part of the organization "{{ organization.name|abbreviate:50 }}"{% endif %}.
{% if organization.sso-active %}
"{{ organization.name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its teams and files now goes
through your organization's identity provider.
If you can't get in, your account probably isn't in the directory yet. To get access, contact the organization owner.
{% endif %}
Accept invitation using this link:
@@ -122,7 +122,7 @@
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
@@ -241,4 +241,4 @@
</div>
</body>
</html>
</html>
@@ -0,0 +1,231 @@
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>
</title>
<!--[if !mso]><!-- -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
.mj-column-px-425 {
width: 425px !important;
max-width: 425px;
}
}
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="background-color:#E5E5E5;">
<div style="background-color:#E5E5E5;">
<!--[if mso | IE]>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:97px;">
<img height="32" src="{{ public-uri }}/images/email/logo-penpot.svg"
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
width="97" />
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<table
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
>
<tr>
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
<![endif]-->
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]>
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
<tr>
<td
class="" style="vertical-align:top;width:600px;"
>
<![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
Hi,
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
"{{ organization-name|abbreviate:50 }}" has set up single sign-on (SSO) in Penpot. Access to its
teams and files now goes through your organization's identity provider.
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
If you can't get in, your account probably isn't in the directory yet. To get access, contact the
organization owner.
</div>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
The Penpot team.</div>
</td>
</tr>
</table>
</div>
<!--[if mso | IE]>
</td>
</tr>
</table>
<![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
{% include "app/email/includes/footer.html" %}
</div>
</body>
</html>
@@ -0,0 +1 @@
“{{ organization-name|abbreviate:25 }}” uses single sign-on
Loaded 100 of 1317 files, more files were not shown because too many files have changed in this diff. Show more