From 2808268e527fd7701a10440e621afdfc28496d6c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 3 Jun 2026 14:36:05 +0200 Subject: [PATCH 1/3] :paperclip: Update changelog --- .opencode/skills/update-changelog/SKILL.md | 11 ++++++-- CHANGES.md | 2 ++ tools/gh.py | 30 ++++++++++++++++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/.opencode/skills/update-changelog/SKILL.md b/.opencode/skills/update-changelog/SKILL.md index 3ebe266f35..9789eb2f18 100644 --- a/.opencode/skills/update-changelog/SKILL.md +++ b/.opencode/skills/update-changelog/SKILL.md @@ -49,6 +49,7 @@ python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog" - `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 +- **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):** In addition to issue-level exclusions, PRs with these labels should be @@ -57,7 +58,9 @@ excluded regardless of their linked issue's labels: - `no issue required` — Trivial fix not tracked as an issue The script outputs JSON with each entry containing `number`, `title`, `state`, -`issue_type`, `labels`, and `closing_prs` (the PRs that fix each issue). +`issue_type`, `labels`, `closing_prs` (the PRs that fix each issue), and +`project_status` (the "Main" project board status, e.g. "Done", "Rejected", +or `null` if not tracked in a project). ### 3. Identify missing entries (optional) @@ -426,7 +429,11 @@ if closed: between the description and the issue link. Use the **PR author** (not the issue author) for the attribution. - **Only closed issues.** An issue must have `state: "closed"` to appear in - the changelog. Open unresolved issues are omitted. + the changelog. Open/unresolved issues are omitted. +- **Rejected project status.** Issues marked as "Rejected" in the "Main" + project board are automatically excluded by `gh.py`, even if they are + closed. The project status is distinct from the GitHub issue state. + Use `--include-rejected` to override this behavior. - **Excluded issues.** Issues with `no changelog` label must be excluded. Issues with `issue_type: "Task"` must also be excluded — they are internal chores, not user-facing changes. diff --git a/CHANGES.md b/CHANGES.md index c7ec370273..a47c1f2cd1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -59,6 +59,7 @@ - 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)) ### :bug: Bugs fixed - Add Shift+Numpad aliases for zoom shortcuts (by @RenzoMXD) [#2457](https://github.com/penpot/penpot/issues/2457) (PR: [#9063](https://github.com/penpot/penpot/pull/9063)) @@ -139,6 +140,7 @@ - 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)) ## 2.15.4 diff --git a/tools/gh.py b/tools/gh.py index 40433791c5..135578298b 100755 --- a/tools/gh.py +++ b/tools/gh.py @@ -115,6 +115,16 @@ query($owner: String!, $repo: String!, $milestone: Int!, $cursor: String) { issueType { name } labels(first: 20) { nodes { name } } closedByPullRequestsReferences(first: 5) { nodes { number } } + projectItems(first: 10) { + nodes { + project { title } + fieldValueByName(name: "Status") { + ... on ProjectV2ItemFieldSingleSelectValue { + name + } + } + } + } } } } @@ -154,6 +164,14 @@ def fetch_milestone_issues(milestone_num: int, states: str) -> list[dict]: if node is None: continue issue_type = node.get("issueType") + # Extract project status from the "Main" project board (if present) + project_status = None + for pi in (node.get("projectItems") or {}).get("nodes") or []: + project = pi.get("project") or {} + if project.get("title") == "Main": + status_field = pi.get("fieldValueByName") or {} + project_status = status_field.get("name") + break all_nodes.append({ "number": node["number"], "title": node["title"], @@ -161,6 +179,7 @@ def fetch_milestone_issues(milestone_num: int, states: str) -> list[dict]: "issue_type": issue_type["name"] if issue_type else None, "labels": [lbl["name"] for lbl in node["labels"]["nodes"]], "closing_prs": [pr["number"] for pr in node["closedByPullRequestsReferences"]["nodes"]], + "project_status": project_status, }) total = len(all_nodes) @@ -210,6 +229,13 @@ def cmd_issues(args: argparse.Namespace) -> None: print(f"After excluding labels: {len(filtered)} issues", file=sys.stderr) issues = filtered + # Filter out issues with "Rejected" project status (unless --include-rejected) + if not args.include_rejected: + rejected = [iss for iss in issues if iss.get("project_status") == "Rejected"] + if rejected: + issues = [iss for iss in issues if iss.get("project_status") != "Rejected"] + print(f"After excluding rejected: {len(issues)} issues (removed {len(rejected)}: {[r['number'] for r in rejected]})", file=sys.stderr) + # Filter to issues NOT yet in the comparison file (if --compare given) if args.compare: existing_nums = load_existing_issue_numbers(args.compare) @@ -452,6 +478,10 @@ def main() -> None: "--compare", help="Path to CHANGES.md; only show issues NOT yet referenced in that file" ) + p_issues.add_argument( + "--include-rejected", action="store_true", + help="Include issues with 'Rejected' project status (excluded by default)" + ) p_issues.set_defaults(func=cmd_issues) # --- prs --- From 7e669290105b557b0646f74cccebdd20d54741b3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 3 Jun 2026 16:54:40 +0200 Subject: [PATCH 2/3] :bug: Fix crash when typography token value is an array (#9992) Add guard in parse-composite-typography-value to check if the converted value is a map before attempting map operations. When a typography token has an array value like ["Roboto"], return an invalid-token-value-typography error instead of crashing with IMap.-dissoc protocol error. Add regression test to verify the fix. --- .../src/app/main/data/style_dictionary.cljs | 56 ++++++++++--------- .../tokens/style_dictionary_test.cljs | 25 +++++++++ 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/main/data/style_dictionary.cljs b/frontend/src/app/main/data/style_dictionary.cljs index 280b431046..4045589a4f 100644 --- a/frontend/src/app/main/data/style_dictionary.cljs +++ b/frontend/src/app/main/data/style_dictionary.cljs @@ -302,36 +302,38 @@ {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-typography value)]} :else - (let [converted (js->clj value :keywordize-keys true) - add-keyed-errors (fn [typography-map k errors] - (update typography-map :errors concat (map #(assoc % :typography-key k) errors))) - ;; Separate line-height to process in an extra step - without-line-height (dissoc converted :line-height) - valid-typography (reduce - (fn [acc [k v]] - (let [{:keys [errors value]} (parse-atomic-typography-value k v)] - (if (seq errors) - (add-keyed-errors acc k errors) - (assoc-in acc [:value k] (or value v))))) - {:value {}} - without-line-height) + (let [converted (js->clj value :keywordize-keys true)] + (if-not (map? converted) + {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-typography value)]} + (let [add-keyed-errors (fn [typography-map k errors] + (update typography-map :errors concat (map #(assoc % :typography-key k) errors))) + ;; Separate line-height to process in an extra step + without-line-height (dissoc converted :line-height) + valid-typography (reduce + (fn [acc [k v]] + (let [{:keys [errors value]} (parse-atomic-typography-value k v)] + (if (seq errors) + (add-keyed-errors acc k errors) + (assoc-in acc [:value k] (or value v))))) + {:value {}} + without-line-height) - ;; Calculate line-height based on the resolved font-size and add it back to the map - line-height (when-let [line-height (:line-height converted)] - (-> (parse-sd-token-typography-line-height - line-height - (get-in valid-typography [:value :font-size]) - (get-in valid-typography [:errors :font-size])))) - valid-typography (cond - (:errors line-height) - (add-keyed-errors valid-typography :line-height (:errors line-height)) + ;; Calculate line-height based on the resolved font-size and add it back to the map + line-height (when-let [line-height (:line-height converted)] + (-> (parse-sd-token-typography-line-height + line-height + (get-in valid-typography [:value :font-size]) + (get-in valid-typography [:errors :font-size])))) + valid-typography (cond + (:errors line-height) + (add-keyed-errors valid-typography :line-height (:errors line-height)) - line-height - (assoc-in valid-typography [:value :line-height] line-height) + line-height + (assoc-in valid-typography [:value :line-height] line-height) - :else - valid-typography)] - valid-typography)))) + :else + valid-typography)] + valid-typography)))))) (defn collect-typography-errors [token] (group-by :typography-key (:errors token))) diff --git a/frontend/test/frontend_tests/tokens/style_dictionary_test.cljs b/frontend/test/frontend_tests/tokens/style_dictionary_test.cljs index 1dbe9d12cf..220dd1f36b 100644 --- a/frontend/test/frontend_tests/tokens/style_dictionary_test.cljs +++ b/frontend/test/frontend_tests/tokens/style_dictionary_test.cljs @@ -97,6 +97,31 @@ (-> errors first :error/code))))) (done)))))))) +;; Regression: a composite typography token whose value is a plain +;; array (e.g. ["Roboto"]) instead of a map must not crash with +;; "No protocol method IMap.-dissoc defined for type object". +;; It should return an invalid-token-value-typography error instead. +(t/deftest resolve-tokens-typography-array-value-test + (t/async + done + (t/testing "typography token with array value produces error instead of crashing" + (let [tokens (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :core-set) + :name "core")) + (ctob/add-token (cthi/id :core-set) + (ctob/make-token {:name "typography.bad" + :value ["Roboto"] + :type :typography})) + (ctob/get-all-tokens-map))] + (-> (sd/resolve-tokens tokens) + (rx/sub! + (fn [resolved-tokens] + (t/is (contains? resolved-tokens "typography.bad")) + (t/is (nil? (get-in resolved-tokens ["typography.bad" :resolved-value]))) + (t/is (= :error.style-dictionary/invalid-token-value-typography + (get-in resolved-tokens ["typography.bad" :errors 0 :error/code]))) + (done)))))))) + (t/deftest resolve-tokens-interactive-test (t/async done From 97c3a9facff8d7676e228eacdc9e1c099afad476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Thu, 4 Jun 2026 10:11:58 +0200 Subject: [PATCH 3/3] :whale: Add improvements related to Docker and Podman compatibility (#10012) * :paperclip: Add tests for boolean parser coverage * :whale: Normalize boolean handling in nginx entrypoint * :whale: Quote boolean env vars in compose example (add Podman compatibility) * :fire: Remove deprecated and duplicated nginx.conf file for Storybook --- common/test/common_tests/schema_test.cljc | 17 ++++++++++++++ docker/images/docker-compose.yaml | 7 +++--- docker/images/files/nginx-entrypoint.sh | 13 ++++++++++- docker/images/nginx.storybook.conf | 27 ----------------------- 4 files changed, 33 insertions(+), 31 deletions(-) delete mode 100644 docker/images/nginx.storybook.conf diff --git a/common/test/common_tests/schema_test.cljc b/common/test/common_tests/schema_test.cljc index 6206d5d38b..b94d4ae4a4 100644 --- a/common/test/common_tests/schema_test.cljc +++ b/common/test/common_tests/schema_test.cljc @@ -171,3 +171,20 @@ (t/is (= candidate-2 (encode-j expected))))) +(t/deftest test-boolean + (let [decode-s (sm/decoder ::sm/boolean sm/string-transformer)] + (t/is (= true (decode-s "true"))) + (t/is (= true (decode-s "True"))) + (t/is (= true (decode-s "TrUe"))) + (t/is (= true (decode-s "TRUE"))) + (t/is (= false (decode-s "false"))) + (t/is (= false (decode-s "False"))) + (t/is (= false (decode-s "fAlSe"))) + (t/is (= false (decode-s "FALSE"))) + + (t/is (= true (decode-s "T"))) + (t/is (= false (decode-s "F"))) + (t/is (= true (decode-s "t"))) + (t/is (= false (decode-s "f"))) + (t/is (= true (decode-s "1"))) + (t/is (= false (decode-s "0"))))) diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index cd5c9bf113..f6979e5e43 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -109,6 +109,7 @@ services: << : [*penpot-flags, *penpot-http-body-size, *penpot-public-uri] # Set to "true" on hosts where IPv6 is disabled at kernel boot level. # PENPOT_DISABLE_IPV6_LISTEN: "true" + penpot-backend: image: "penpotapp/backend:${PENPOT_VERSION:-2.15}" restart: always @@ -161,7 +162,7 @@ services: ## based on real scenarios. If you want to help us, please leave it enabled. You can ## audit what data we send with the code available on github. - PENPOT_TELEMETRY_ENABLED: true + PENPOT_TELEMETRY_ENABLED: "true" PENPOT_TELEMETRY_REFERER: compose ## Example SMTP/Email configuration. By default, emails are sent to the mailcatch @@ -175,8 +176,8 @@ services: PENPOT_SMTP_PORT: 1025 PENPOT_SMTP_USERNAME: PENPOT_SMTP_PASSWORD: - PENPOT_SMTP_TLS: false - PENPOT_SMTP_SSL: false + PENPOT_SMTP_TLS: "false" + PENPOT_SMTP_SSL: "false" penpot-mcp: image: "penpotapp/mcp:${PENPOT_VERSION:-2.15}" diff --git a/docker/images/files/nginx-entrypoint.sh b/docker/images/files/nginx-entrypoint.sh index 813d587199..999e14ad3f 100644 --- a/docker/images/files/nginx-entrypoint.sh +++ b/docker/images/files/nginx-entrypoint.sh @@ -1,5 +1,16 @@ #!/usr/bin/env bash +is_truthy() { + local value="${1,,}" + [[ "$value" == "true" || "$value" == "t" || "$value" == "1" ]] +} + +is_falsy() { + local value="${1,,}" + [[ "$value" == "false" || "$value" == "f" || "$value" == "0" ]] +} + + ######################################### ## Air Gapped config ######################################### @@ -45,7 +56,7 @@ export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061} export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000} export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB export PENPOT_IPV6_LISTEN_DIRECTIVE=${PENPOT_IPV6_LISTEN_DIRECTIVE:-"listen [::]:8080 default_server reuseport backlog=16384;"} -if [ "${PENPOT_DISABLE_IPV6_LISTEN}" = "true" ]; then +if is_truthy "${PENPOT_DISABLE_IPV6_LISTEN:-}"; then export PENPOT_IPV6_LISTEN_DIRECTIVE="" fi envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE,\$PENPOT_IPV6_LISTEN_DIRECTIVE" \ diff --git a/docker/images/nginx.storybook.conf b/docker/images/nginx.storybook.conf deleted file mode 100644 index fb7106dc90..0000000000 --- a/docker/images/nginx.storybook.conf +++ /dev/null @@ -1,27 +0,0 @@ -server { - listen 8080 default_server; - server_name _; - - charset utf-8; - etag off; - - gzip on; - gzip_static on; - gzip_types text/plain text/css application/javascript application/json application/vnd.api+json application/xml application/x-javascript text/xml image/svg+xml; - gzip_proxied any; - gzip_comp_level 6; - gzip_buffers 16 8k; - gzip_http_version 1.1; - gzip_min_length 256; - gzip_vary on; - - error_log /dev/stderr; - access_log /dev/stdout; - - root /var/www; - index index.html; - - location / { - try_files $uri $uri/ /index.html; - } -}