Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2026-08-27 10:05:43 +02:00
commit 17befc1db9
7 changed files with 212 additions and 3 deletions

View File

@ -212,6 +212,37 @@ superseded it:
Replace the reference in the changelog entry with the correct merged PR number.
### 5b. Security advisory (GHSA) entries
Security advisories fixed in a release are documented in the changelog even
though they are **neither milestone issues nor PRs**. The GHSA ID and its
description are supplied by the user or the release notes — they never come
from the milestone fetch in step 2.
**Format** (matches the existing precedent in `CHANGES.md`, e.g. the
`create-font-variant` arbitrary file read advisory):
```markdown
- Fix <user-facing description> (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX)
```
Rules:
- Place the entry under `### :bug: Bugs fixed`, with **no issue or PR link**
only the advisory URL.
- The advisory may be **draft/unpublished** at changelog time (the URL 404s
publicly). Do **not** web-fetch or verify the URL, and do **not** drop the
entry because of that. Rely on the GHSA ID provided by the user.
- Derive the description from the supplied advisory title, imperative mood and
user-facing (e.g. `Fix command injection in SVG exporter via legacy fill-color`).
- These entries are **invisible to the automation**: they are not returned by
`gh.py issues`, not matched by `--compare` (step 3), not part of the PR
cross-reference (step 10), and not scanned by the anomaly-report regexes
(step 11, which only match `issues/` and `pull/` links). Add them manually.
- During pre-flight checks (step 6a) apply only the **backport/duplicate**
check: if the same GHSA already appears in an earlier version section, remove
it from the current section. Their absence from milestone cross-references
is expected, not an anomaly.
### 6. Read the current CHANGES.md
Read the top of `CHANGES.md` to understand the existing format and find the
@ -400,6 +431,8 @@ if closed:
- ✅ 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
- ✅ Advisory (GHSA) entries are not milestone PRs — their absence from the
cross-reference is intentional (see step 5b)
## Version section template
@ -410,8 +443,12 @@ if closed:
- <fix description> [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
- <fix description> (by @contributor) [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
- <fix description> (https://github.com/penpot/penpot/security/advisories/GHSA-XXXX-XXXX-XXXX)
```
Advisory (GHSA) entries have no issue or PR link — just the advisory URL. See
step 5b.
### 11. Generate anomaly report and save to CHANGES-ISSUES.md
After all edits and cross-referencing are complete, generate a structured
@ -732,6 +769,14 @@ self-contained and clickable in any Markdown viewer.
Taiga description text or by searching GitHub PRs that reference the Taiga
URL. Replace the Taiga reference with the GitHub issue link and add the PR
reference if applicable.
- **Security advisory (GHSA) entries.** Advisories fixed in the release are
listed under `### :bug: Bugs fixed` with the advisory URL and **no issue or
PR link**, even though they are not in the milestone. The GHSA ID and
description come from the user — do **not** fetch or verify the URL, and do
not drop a draft (unpublished) advisory. Precedent:
`- Fix arbitrary file read security issue on create-font-variant rpc method
(https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)`.
See step 5b.
- **Re-fetch before editing.** Milestones can change — always re-fetch issues
before making edits, don't rely on cached data.
- **Use `scripts/gh.py`.** Prefer the helper script over raw `gh api` calls for

View File

@ -23,6 +23,13 @@
- 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.2
### :bug: Bugs fixed
- Fix linear gradients in SVG text exports being emitted as radial gradients [#5972](https://github.com/penpot/penpot/issues/5972) (PR: [#11272](https://github.com/penpot/penpot/pull/11272))
- Fix typography token becoming detached when editing text content [#11362](https://github.com/penpot/penpot/issues/11362) (PR: [#11366](https://github.com/penpot/penpot/pull/11366))
- Fix command injection in SVG exporter via legacy fill-color (https://github.com/penpot/penpot/security/advisories/GHSA-4f36-m4hj-cv86)
## 2.17.1

View File

@ -1978,6 +1978,26 @@
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "0026-fix-svg-raw-shapes-uuids"
;; Before the svg-raw schema declared :shapes as a vector of uuid,
;; the JSON decoder had no type information for those child ids and
;; left them as plain strings on any round trip, so they got
;; persisted as strings. Once the schema was tightened, such files
;; fail schema validation; this migration parses the strings back
;; into uuid instances.
[data _]
(letfn [(update-object [object]
(cond-> object
(cfh/svg-raw-shape? object)
(d/update-when :shapes #(mapv uuid/coerce %))))
(update-container [container]
(d/update-when container :objects d/update-vals update-object))]
(-> data
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(def available-migrations
(into (d/ordered-set)
["legacy-2"
@ -2060,4 +2080,5 @@
"0022-normalize-component-root-and-resync"
"0023-repair-token-themes-with-inexistent-sets"
"0024b-fix-stroke-cap-placement"
"0025-repair-empty-text-content"]))
"0025-repair-empty-text-content"
"0026-fix-svg-raw-shapes-uuids"]))

View File

@ -9,6 +9,7 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.flags :as flags]
[app.common.math :as mth]
[app.common.types.color :as clr]
[app.common.types.fills :as types.fills]
[clojure.set :as set]
@ -217,7 +218,10 @@
attributes or other things that may be attached).
- Consider nil values, empty strings or empty lists all equal.
- Normalize numeric values (legacy) into strings.
- No value is equal than the default value."
- No value is equal than the default value.
- Numeric attrs (e.g. line-height) compare with float tolerance so
editor/WASM round-trips like \"1.3333333333333333\" vs \"1.33333\"
do not count as a real style change (avoids detaching tokens)."
[key value1 value2]
(when (text-node-attr? key)
(let [default-value (get default-text-attrs key)
@ -229,7 +233,16 @@
$)))
value1' (normalize-value value1)
value2' (normalize-value value2)]
(not= value1' value2'))))
(cond
(= value1' value2')
false
:else
(let [n1 (when (string? value1') (d/parse-double value1'))
n2 (when (string? value2') (d/parse-double value2'))]
(if (and (some? n1) (some? n2))
(not (mth/close? n1 n2))
true))))))
(defn- compare-text-content
"Given two content text structures, conformed by maps and vectors,

View File

@ -0,0 +1,110 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns common-tests.files-migrations-0026-test
(:require
[app.common.files.migrations :as cfm]
[app.common.uuid :as uuid]
[clojure.test :as t]))
;; 0026-fix-svg-raw-shapes-uuids
;; Before the svg-raw schema declared :shapes as a vector of uuid, the
;; JSON decoder had no type information for those child ids and left
;; them as plain strings on any round trip, so they got persisted as
;; strings. Once the schema was tightened, such files fail schema
;; validation; this migration parses the strings back into uuids.
(defn- make-svg-raw-shape
"Build a minimal svg-raw shape with the supplied :shapes vector.
When `shapes` is nil the :shapes key is omitted, like a leaf svg-raw
shape."
[shape-id shapes]
(cond-> {:id shape-id
:type :svg-raw}
(some? shapes)
(assoc :shapes shapes)))
(defn- make-other-shape
"Build a minimal non-svg-raw shape that must stay untouched."
[shape-id shapes]
{:id shape-id
:type :group
:shapes shapes})
(t/deftest migration-0026-converts-svg-raw-shapes-strings-to-uuids-in-pages
(let [shape-id (uuid/next)
child-id (uuid/next)
page-id (uuid/next)
data {:pages-index
{page-id
{:objects
{shape-id (make-svg-raw-shape
shape-id
[(str child-id)
"1c2986ce-4a0f-8001-8007-1fb8f3b5ab31"])}}}}
data' (cfm/migrate-data data "0026-fix-svg-raw-shapes-uuids")
shape (get-in data' [:pages-index page-id :objects shape-id])]
(t/is (= 2 (count (:shapes shape))) "child ids preserved")
(t/is (= child-id (first (:shapes shape))) "existing uuid string parsed to uuid")
(t/is (= #uuid "1c2986ce-4a0f-8001-8007-1fb8f3b5ab31" (second (:shapes shape)))
"foreign uuid string parsed to uuid")
(t/is (every? uuid? (:shapes shape)) "all child ids are uuids")))
(t/deftest migration-0026-converts-svg-raw-shapes-strings-to-uuids-in-components
(let [shape-id (uuid/next)
child-id (uuid/next)
component-id (uuid/next)
data {:components
{component-id
{:objects
{shape-id (make-svg-raw-shape
shape-id
[(str child-id)
"1c2986ce-4a0f-8001-8007-1fb92196e65f"])}}}}
data' (cfm/migrate-data data "0026-fix-svg-raw-shapes-uuids")
shape (get-in data' [:components component-id :objects shape-id])]
(t/is (= 2 (count (:shapes shape))) "child ids preserved")
(t/is (= child-id (first (:shapes shape))) "existing uuid string parsed to uuid")
(t/is (= #uuid "1c2986ce-4a0f-8001-8007-1fb92196e65f" (second (:shapes shape)))
"foreign uuid string parsed to uuid")
(t/is (every? uuid? (:shapes shape)) "all child ids are uuids")))
(t/deftest migration-0026-leaves-uuids-and-other-shapes-untouched
(let [svg-raw-id (uuid/next)
child-id (uuid/next)
group-id (uuid/next)
leaf-id (uuid/next)
page-id (uuid/next)
data {:pages-index
{page-id
{:objects
{svg-raw-id (make-svg-raw-shape svg-raw-id [child-id])
group-id (make-other-shape group-id [(str child-id)])
leaf-id (make-svg-raw-shape leaf-id nil)}}}}
data' (cfm/migrate-data data "0026-fix-svg-raw-shapes-uuids")
objects (get-in data' [:pages-index page-id :objects])]
(t/is (= [child-id] (:shapes (get objects svg-raw-id)))
"already-uuid svg-raw children untouched")
(t/is (= [(str child-id)] (:shapes (get objects group-id)))
"non-svg-raw shapes untouched")
(t/is (nil? (:shapes (get objects leaf-id)))
"svg-raw leaf without :shapes untouched")))
(t/deftest migration-0026-is-idempotent
(let [shape-id (uuid/next)
child-id (uuid/next)
page-id (uuid/next)
data {:pages-index
{page-id
{:objects
{shape-id (make-svg-raw-shape shape-id [(str child-id)])}}}}
data' (cfm/migrate-data data "0026-fix-svg-raw-shapes-uuids")
data'' (cfm/migrate-data data' "0026-fix-svg-raw-shapes-uuids")]
(t/is (= data' data'') "second run is a no-op")))

View File

@ -19,6 +19,7 @@
[common-tests.files-builder-test]
[common-tests.files-changes-test]
[common-tests.files-migrations-0025-test]
[common-tests.files-migrations-0026-test]
[common-tests.files-migrations-test]
[common-tests.files.shapes-builder-test]
[common-tests.files.validate-test]
@ -97,6 +98,7 @@
'common-tests.files-changes-test
'common-tests.files-builder-test
'common-tests.files-migrations-0025-test
'common-tests.files-migrations-0026-test
'common-tests.files-migrations-test
'common-tests.files.validate-test
'common-tests.geom-align-test

View File

@ -78,6 +78,14 @@
(def content-changed-line-height
(assoc-in content-base [:children 0 :children 0 :line-height] "1.5"))
;; Token/WASM may store full float precision; editor round-trips often
;; truncate (e.g. CSS / f32). These must compare as equal.
(def content-line-height-full-precision
(assoc-in content-base [:children 0 :children 0 :line-height] "1.3333333333333333"))
(def content-line-height-truncated
(assoc-in content-base [:children 0 :children 0 :line-height] "1.33333"))
(def content-redundant-span-line-height
(assoc-in content-base [:children 0 :children 0 :children 0 :line-height] "1.5"))
@ -208,6 +216,8 @@
;; Other text-node-attr categories
attrs-font-family (cttx/get-diff-attrs content-base content-changed-font-family)
attrs-line-height (cttx/get-diff-attrs content-base content-changed-line-height)
attrs-line-height-precision (cttx/get-diff-attrs content-line-height-full-precision
content-line-height-truncated)
attrs-span-line-height (cttx/get-diff-attrs content-base content-redundant-span-line-height)
attrs-roundtrip-line-height (cttx/get-diff-attrs content-token-like-line-height
content-after-editor-roundtrip)
@ -242,6 +252,7 @@
;; Each text-node-attr category reports correct attr key
(t/is (= #{:font-family} attrs-font-family))
(t/is (= #{:line-height} attrs-line-height))
(t/is (= #{} attrs-line-height-precision))
(t/is (= #{} attrs-span-line-height))
(t/is (= #{} attrs-roundtrip-line-height))
(t/is (= #{} attrs-nil-typography-refs))