From 39ca4c264c398b667d8502fe0f7c0b70e651d061 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 15 Sep 2026 14:14:32 +0200 Subject: [PATCH 1/6] :fire: Remove onboarding A/B test and welcome file creation (#11707) Drop the onboarding-03 experiment consulted through external-feature-flag, keeping the false-branch behavior: registration never requests a welcome file and the workspace never shows the onboarding modals. Remove the now-unused welcome-file machinery on the backend (RPC wiring, welcome_file namespace, welcome-file-id prop and the post-login redirect). Keep the external-feature-flag helper as the seam for future experiments and note it in mem:frontend/core. Closes #11705 AI-assisted-by: muse-spark-1.3-contributor --- .serena/memories/frontend/core.md | 1 + backend/src/app/rpc/commands/auth.clj | 17 +----- backend/src/app/rpc/commands/profile.clj | 1 - backend/src/app/setup/welcome_file.clj | 67 --------------------- frontend/src/app/main/data/auth.cljs | 28 ++++----- frontend/src/app/main/ui.cljs | 24 ++------ frontend/src/app/main/ui/auth/register.cljs | 17 +----- 7 files changed, 22 insertions(+), 133 deletions(-) delete mode 100644 backend/src/app/setup/welcome_file.clj diff --git a/.serena/memories/frontend/core.md b/.serena/memories/frontend/core.md index b5fb47dce4..1233aaaf31 100644 --- a/.serena/memories/frontend/core.md +++ b/.serena/memories/frontend/core.md @@ -13,6 +13,7 @@ Frontend: CLJS SPA; React/Rumext; Potok; RxJS; okulary refs; SCSS modules; share - `app.util.*`: DOM, HTTP, i18n, keyboard, codegen, and general frontend utilities. - `frontend/packages/*` and `frontend/text-editor`: JS/TS workspace packages consumed by the app. - Nitrate subscription/organization UI and flows live under `app.main.data.nitrate` and `app.main.ui.nitrate*`; backend/API behavior is covered by backend memories, and shared permission rules are in `common/src/app/common/types/nitrate_permissions.cljc`. +- `app.config` external bridges (`external-feature-flag`, `external-session-id`, `external-context-info`, `external-notify-register-success`, `initialize-external-context-info`) delegate to `globalThis` hooks injected by the SaaS host. Never delete `external-feature-flag`, even with zero call sites: it is the seam for future A/B tests. ## Lint and Format diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 78d2ac45c4..14119c819b 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -33,11 +33,9 @@ [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.setup :as-alias setup] - [app.setup.welcome-file :refer [create-welcome-file]] [app.storage :as sto] [app.tokens :as tokens] [app.util.services :as sv] - [app.worker :as wrk] [cuerdas.core :as str])) (def schema:password @@ -308,7 +306,6 @@ [:fullname ::sm/text] [:email ::sm/email] [:password schema:password] - [:create-welcome-file {:optional true} :boolean] [:accept-newsletter-updates {:optional true} :boolean] [:invitation-token {:optional true} schema:token]]) @@ -446,7 +443,7 @@ :extra-data ptoken})))) (defn register-profile - [{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token] :as params}] + [{:keys [::db/conn] :as cfg} {:keys [token] :as params}] (let [claims (tokens/verify cfg {:token token :iss :prepared-register}) params (cond-> claims (:accept-newsletter-updates params) @@ -469,14 +466,7 @@ (tokens/verify cfg {:token token :iss :team-invitation})) props (-> (audit/profile->props profile) - (assoc :from-invitation (some? invitation))) - - - create-welcome-file-when-needed - (fn [] - (when (:create-welcome-file params) - (let [cfg (dissoc cfg ::db/conn)] - (wrk/submit! executor (create-welcome-file cfg profile)))))] + (assoc :from-invitation (some? invitation)))] (cond ;; When profile is blocked, we just ignore it and return plain data @@ -525,7 +515,6 @@ :email (:email profile) :invitation-token token} (rph/with-transform (session/create-fn cfg profile claims)) - (rph/with-defer create-welcome-file-when-needed) (rph/with-meta {::audit/replace-props props ::audit/context {:action "accept-invitation"} ::audit/profile-id (:id profile)}))) @@ -533,7 +522,6 @@ (:is-active profile) (-> (profile/strip-private-attrs profile) (rph/with-transform (session/create-fn cfg profile claims)) - (rph/with-defer create-welcome-file-when-needed) (rph/with-meta {::audit/replace-props props ::audit/context {:action "login"} @@ -548,7 +536,6 @@ (-> {:id (:id profile) :email (:email profile)} - (rph/with-defer create-welcome-file-when-needed) (rph/with-meta {::audit/replace-props props ::audit/context {:action "email-verification"} diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 22c7e3dfa5..346f275e9f 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -69,7 +69,6 @@ [:onboarding-questions-answered {:optional true} ::sm/boolean] [:nitrate-onboarding-viewed {:optional true} ::sm/boolean] [:v2-info-shown {:optional true} ::sm/boolean] - [:welcome-file-id {:optional true} [:maybe ::sm/boolean]] [:release-notes-viewed {:optional true} [::sm/text {:max 100}]] [:notifications {:optional true} schema:props-notifications] diff --git a/backend/src/app/setup/welcome_file.clj b/backend/src/app/setup/welcome_file.clj deleted file mode 100644 index 48887e53ed..0000000000 --- a/backend/src/app/setup/welcome_file.clj +++ /dev/null @@ -1,67 +0,0 @@ -;; 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 app.setup.welcome-file - (:require - [app.common.logging :as l] - [app.db :as db] - [app.rpc :as-alias rpc] - [app.rpc.climit :as-alias climit] - [app.rpc.commands.files :as files] - [app.rpc.commands.files-update :as fupdate] - [app.rpc.commands.management :as management] - [app.rpc.commands.profile :as profile] - [app.rpc.doc :as-alias doc] - [app.setup :as-alias setup] - [app.setup.templates :as tmpl] - [app.worker :as-alias wrk])) - -(def ^:private page-id #uuid "2c6952ee-d00e-8160-8004-d2250b7210cb") -(def ^:private shape-id #uuid "765e9f82-c44e-802e-8004-d72a10b7b445") - -(def ^:private update-path - [:data :pages-index page-id :objects shape-id - :content :children 0 :children 0 :children 0]) - -(def ^:private sql:mark-file-object-thumbnails-deleted - "UPDATE file_tagged_object_thumbnail - SET deleted_at = now() - WHERE file_id = ?") - -(def ^:private sql:mark-file-thumbnail-deleted - "UPDATE file_thumbnail - SET deleted_at = now() - WHERE file_id = ?") - -(defn- update-welcome-shape - [_ file name] - (let [text (str "Welcome to Penpot, " name "!")] - (-> file - (update-in update-path assoc :text text) - (update-in [:data :pages-index page-id :objects shape-id] assoc :name "Welcome to Penpot!") - (update-in [:data :pages-index page-id :objects shape-id] dissoc :position-data)))) - -(defn create-welcome-file - [cfg {:keys [id fullname] :as profile}] - (try - (let [cfg (dissoc cfg ::db/conn) - params {:profile-id (:id profile) - :project-id (:default-project-id profile)} - template-stream (tmpl/get-template-stream cfg "welcome") - file-id (-> (management/clone-template cfg params template-stream) - first) - file-name (str fullname "'s first file")] - - (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] - (files/rename-file conn {:id file-id :name file-name}) - (fupdate/update-file! cfg file-id update-welcome-shape fullname) - (profile/update-profile-props cfg id {:welcome-file-id file-id}) - (db/exec-one! conn [sql:mark-file-object-thumbnails-deleted file-id]) - (db/exec-one! conn [sql:mark-file-thumbnail-deleted file-id])))) - - (catch Throwable cause - (l/error :hint "unexpected error on create welcome file " :cause cause)))) - diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs index 2339c02452..05bd5e9621 100644 --- a/frontend/src/app/main/data/auth.cljs +++ b/frontend/src/app/main/data/auth.cljs @@ -37,7 +37,7 @@ "This is the main event that is executed once we have logged in profile. The profile can proceed from standard login or from accepting invitation, or third party auth signup or singin." - [{:keys [props] :as profile}] + [profile] (letfn [(get-redirect-events [teams] (if-let [token (:invitation-token profile)] (rx/of (rt/nav :auth-verify-token {:token token})) @@ -47,22 +47,16 @@ (if (= redirect-href (rt/get-current-href)) (rx/of (rt/reload true)) (rx/of (rt/nav-raw :href redirect-href)))) - (if-let [file-id (get props :welcome-file-id)] - (rx/of (dcm/go-to-workspace - :file-id file-id - :team-id (:default-team-id profile)) - (dp/update-profile-props {:welcome-file-id nil})) - - (let [default-team-id (:default-team-id profile) - team-ids (into #{} (map :id) teams) - team-id (dtm/get-last-team-id) - team-id (if (and team-id (contains? team-ids team-id)) - team-id - default-team-id)] - (->> (dtm/resolve-login-team-id {:team-id team-id - :default-team-id default-team-id}) - (rx/mapcat (fn [team-id] - (rx/of (dcm/go-to-dashboard-recent {:team-id team-id}))))))))))] + (let [default-team-id (:default-team-id profile) + team-ids (into #{} (map :id) teams) + team-id (dtm/get-last-team-id) + team-id (if (and team-id (contains? team-ids team-id)) + team-id + default-team-id)] + (->> (dtm/resolve-login-team-id {:team-id team-id + :default-team-id default-team-id}) + (rx/mapcat (fn [team-id] + (rx/of (dcm/go-to-dashboard-recent {:team-id team-id})))))))))] (ptk/reify ::logged-in ptk/WatchEvent diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 58175de604..dac8975f1f 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -270,24 +270,12 @@ file-id (some-> params :file-id uuid/parse*) page-id (some-> params :page-id uuid/parse*) layout (some-> params :layout keyword)] - [:? {} - (when (cf/external-feature-flag "onboarding-03" "test") - (cond - show-question-modal? - [:& questions-modal] - - show-team-modal? - [:> onboarding-team-modal* {:go-to-team false}] - - show-release-modal? - [:& release-notes-modal {:version (:main cf/version)}])) - - [:> team-container* {:team-id team-id} - [:> workspace-page* {:team-id team-id - :file-id file-id - :page-id page-id - :layout-name layout - :key file-id}]]]) + [:> team-container* {:team-id team-id} + [:> workspace-page* {:team-id team-id + :file-id file-id + :page-id page-id + :layout-name layout + :key file-id}]]) :viewer (let [params (get params :query) diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index da3438640f..1b8fc40f10 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -155,14 +155,7 @@ (mf/deps on-success-callback) (fn [form _event] (reset! submitted? true) - (let [create-welcome-file? - (cf/external-feature-flag "onboarding-03" "test") - - cdata - (cond-> (:clean-data @form) - create-welcome-file? - (assoc :create-welcome-file true))] - + (let [cdata (:clean-data @form)] (->> (rp/cmd! :prepare-register-profile cdata) (rx/subs! on-register-profile on-error #(reset! submitted? false))))))] @@ -321,13 +314,7 @@ (mf/deps on-success on-error) (fn [form _] (reset! submitted? true) - (let [create-welcome-file? - (cf/external-feature-flag "onboarding-03" "test") - - params - (cond-> (:clean-data @form) - create-welcome-file? (assoc :create-welcome-file true))] - + (let [params (:clean-data @form)] (->> (rp/cmd! :register-profile params) (rx/finalize #(reset! submitted? false)) (rx/subs! on-success on-error)))))] From 1f64cc3e5244333bb4ced7a0d1fcc5a2f7886293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 15 Sep 2026 18:55:11 +0200 Subject: [PATCH 2/6] :bug: Make integration test summary non-fatal on staging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The summary step reads a filter from .github/scripts, which never landed on this branch, so every run failed with exit code 2 even when all tests passed. Add the filter and tolerate both a missing file and a jq error. Use `!cancelled()` instead of `always()` on the merge job, so it no longer runs after cancel-in-progress killed the shards without uploading their blobs. Lower the JSON report retention to 7 days, the repository maximum. Signed-off-by: David Barragán Merino --- .github/scripts/playwright-summary.jq | 41 +++++++++++++++++++++++++ .github/workflows/tests-integration.yml | 12 +++++--- 2 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 .github/scripts/playwright-summary.jq diff --git a/.github/scripts/playwright-summary.jq b/.github/scripts/playwright-summary.jq new file mode 100644 index 0000000000..034b582267 --- /dev/null +++ b/.github/scripts/playwright-summary.jq @@ -0,0 +1,41 @@ +def specs: [.. | objects | select(has("tests") and has("file"))]; +def dur: [.tests[].results[]?.duration // 0] | add; + +specs as $s +| ($s | map(select(any(.tests[]; .status == "unexpected")))) as $failed +| ($s | map(select(any(.tests[]; .status == "flaky")))) as $flaky +| ($s | map(select(any(.tests[]; .status == "skipped")))) as $skipped +| ($s | length) as $total +| ($s | map(dur) | add // 0 | . / 1000 | floor) as $cpu +| (if ($failed | length) > 0 then "❌" + elif ($flaky | length) > 0 then "⚠️" + else "✅" end) as $icon + +| "## \($icon) Integration tests\n\n" ++ "| Total | Passed | Flaky | Failed | Skipped | Test time |\n" ++ "|---|---|---|---|---|---|\n" ++ "| \($total) | \($total - ($failed|length) - ($flaky|length) - ($skipped|length)) " ++ "| \($flaky|length) | \($failed|length) | \($skipped|length) | \($cpu / 60 | floor)m |\n" + ++ (if ($failed | length) > 0 then + "\n### Failed\n\n" + + ($failed | map("- `\(.file):\(.line)` — \(.title)") | join("\n")) + "\n" + else "" end) + ++ (if ($flaky | length) > 0 then + "\n### Flaky (passed on retry)\n\n" + + ($flaky + | map({ t: "`\(.file):\(.line)` — \(.title)", + r: ([.tests[].results[]? | select(.status == "failed")] | length) }) + | sort_by(-.r) + | map("- \(.t) _(\(.r) \(if .r == 1 then "retry" else "retries" end))_") + | join("\n")) + "\n" + else "" end) + ++ (if $total > 0 then + "\n
Slowest specs\n\n" + + ($s | map({ t: "`\(.file)` — \(.title)", d: (dur / 1000 | floor) }) + | sort_by(-.d) | .[0:5] + | map("- \(.t) — \(.d)s") | join("\n")) + + "\n\n
\n" + else "" end) diff --git a/.github/workflows/tests-integration.yml b/.github/workflows/tests-integration.yml index 343e0dcef6..5c2a4bcc98 100644 --- a/.github/workflows/tests-integration.yml +++ b/.github/workflows/tests-integration.yml @@ -178,7 +178,7 @@ jobs: retention-days: 3 merge-reports: - if: ${{ always() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }} + if: ${{ !cancelled() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }} name: "Merge Integration Reports" runs-on: penpot-extended-runner timeout-minutes: 15 @@ -221,16 +221,18 @@ jobs: - name: Test summary if: always() + continue-on-error: true working-directory: ./frontend run: | if [ ! -f report.json ]; then echo "No report produced (all shards failed early)." >> "$GITHUB_STEP_SUMMARY" exit 0 fi - jq -r -f ../.github/scripts/playwright-summary.jq report.json >> "$GITHUB_STEP_SUMMARY" + jq -r -f ../.github/scripts/playwright-summary.jq report.json \ + >> "$GITHUB_STEP_SUMMARY" \ + || echo "Summary generation failed; see the HTML report artifact." \ + >> "$GITHUB_STEP_SUMMARY" - # Kept for 30 days so flakiness rates can be aggregated across runs - # without scraping job logs. - name: Upload JSON report uses: actions/upload-artifact@v7 if: always() @@ -239,7 +241,7 @@ jobs: path: frontend/report.json overwrite: true if-no-files-found: ignore - retention-days: 30 + retention-days: 7 - name: Upload HTML report uses: actions/upload-artifact@v7 From cdda6ae33c748c84793f91571cd45b7cebddef3a Mon Sep 17 00:00:00 2001 From: Chulgil Lee Date: Wed, 16 Sep 2026 01:54:58 +0900 Subject: [PATCH 3/6] :bug: Fix form select label resetting to its default option (#11664) select* resets its label to `default-selected` whenever its options change identity. The access token dialog builds its options vector inline, so picking an expiration re-rendered the dialog and the label snapped back to "Never", although the form kept the picked value and the token was created with it. form-select* now passes the form value as `default-selected` when it holds a non-blank string, so the reset lands on the picked option. The caller's default still applies while the form value is blank. Closes #11663 AI-assisted-by: claude-opus-5 Signed-off-by: chulgil <2044587+chulgil@users.noreply.github.com> Co-authored-by: chulgil <2044587+chulgil@users.noreply.github.com> Co-authored-by: Andrey Antukh --- frontend/src/app/main/ui/forms.cljs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/ui/forms.cljs b/frontend/src/app/main/ui/forms.cljs index f6a870f618..0816e88d32 100644 --- a/frontend/src/app/main/ui/forms.cljs +++ b/frontend/src/app/main/ui/forms.cljs @@ -204,7 +204,17 @@ props (mf/spread-props props {:on-change handle-change - :value value})] + :value value}) + + ;; select* resets its label to `default-selected` whenever its + ;; options change identity, and callers usually build the options + ;; inline, so every re-render snapped the label back to the default + ;; while the form kept the picked value. Pass the form value as the + ;; default so that reset lands on the picked option. + props + (if (and (string? value) (not (str/blank? value))) + (mf/spread-props props {:default-selected value}) + props)] [:> select* props])) From 02e573100a88d0adf0aa4ba06f662e354c9ce21c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 15 Sep 2026 19:03:39 +0200 Subject: [PATCH 4/6] :books: Update changelog --- CHANGES.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index fa0e466b70..1ae489b0ab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -87,6 +87,7 @@ - Normalize string inputs before processing [#11060](https://github.com/penpot/penpot/issues/11060) (PR: [#11061](https://github.com/penpot/penpot/pull/11061)) - Add cooldown to avoid sending duplicate invitation emails [#11062](https://github.com/penpot/penpot/issues/11062) (PR: [#11063](https://github.com/penpot/penpot/pull/11063)) - Enable SSRF protection for organization SSO validation [#11064](https://github.com/penpot/penpot/issues/11064) (PR: [#11065](https://github.com/penpot/penpot/pull/11065)) +- Fix LDAP authentication storing client-supplied email instead of directory email and not escaping filter special characters [#11084](https://github.com/penpot/penpot/issues/11084) (PR: [#11085](https://github.com/penpot/penpot/pull/11085)) - Fix clone-file-media-object allowing to clone media objects from files without read access [#11087](https://github.com/penpot/penpot/issues/11087) (PR: [#11090](https://github.com/penpot/penpot/pull/11090)) - Fix 404 error page logo not visible in dark mode [#11091](https://github.com/penpot/penpot/issues/11091) (PR: [#11167](https://github.com/penpot/penpot/pull/11167)) - Fix incorrect permission handling when creating an invitation [#11098](https://github.com/penpot/penpot/issues/11098) (PR: [#11099](https://github.com/penpot/penpot/pull/11099)) @@ -111,19 +112,27 @@ - Fix get-team-invitation-token requiring only read permissions [#11358](https://github.com/penpot/penpot/issues/11358) (PR: [#11359](https://github.com/penpot/penpot/pull/11359)) - Fix missing text in legacy SVG board thumbnails [#10182](https://github.com/penpot/penpot/issues/10182) (PR: [#11552](https://github.com/penpot/penpot/pull/11552)) - Fix workspace crash when applying transform modifiers in the WASM renderer [#10894](https://github.com/penpot/penpot/issues/10894) (PR: [#10896](https://github.com/penpot/penpot/pull/10896)) +- Fix double-click on text bounding box entering edit mode instead of applying auto-width/auto-height with the WebGL renderer [#10934](https://github.com/penpot/penpot/issues/10934) (PR: [#11068](https://github.com/penpot/penpot/pull/11068)) - Limit ZIP entry count and object size on V3 binfile import [#11021](https://github.com/penpot/penpot/issues/11021) (PR: [#11022](https://github.com/penpot/penpot/pull/11022)) - Block plugin UI iframe URLs targeting the Penpot domain [#11271](https://github.com/penpot/penpot/issues/11271) (PR: [#11273](https://github.com/penpot/penpot/pull/11273)) - Restrict the MCP REPL code execution endpoint to development environments [#11283](https://github.com/penpot/penpot/issues/11283) (PR: [#11282](https://github.com/penpot/penpot/pull/11282)) - Filter share-link tokens from the get-view-only-bundle response [#11285](https://github.com/penpot/penpot/issues/11285) (PR: [#11286](https://github.com/penpot/penpot/pull/11286)) +- Fix HTTP error responses disclosing internal database and filesystem paths to API clients [#11287](https://github.com/penpot/penpot/issues/11287) (PR: [#11288](https://github.com/penpot/penpot/pull/11288)) - Disable MCP developer tools in multi-user mode [#11291](https://github.com/penpot/penpot/issues/11291) (PR: [#11310](https://github.com/penpot/penpot/pull/11310)) - Fix Hide comments setting being ignored after opening the Comments section [#11308](https://github.com/penpot/penpot/issues/11308) (PR: [#11492](https://github.com/penpot/penpot/pull/11492)) - Block NAT64/6to4/Teredo IPv6 transition addresses in the SSRF guard [#11319](https://github.com/penpot/penpot/issues/11319) (PR: [#11320](https://github.com/penpot/penpot/pull/11320)) - Prevent team admins from removing the team owner [#11367](https://github.com/penpot/penpot/issues/11367) (PR: [#11368](https://github.com/penpot/penpot/pull/11368)) - Enforce share-link comment permissions and page scope [#11370](https://github.com/penpot/penpot/issues/11370) (PR: [#11371](https://github.com/penpot/penpot/pull/11371)) - Clean up orphaned teams, projects and files on profile deletion [#11394](https://github.com/penpot/penpot/issues/11394) (PR: [#11395](https://github.com/penpot/penpot/pull/11395)) +- Use constant-time comparison for Management API shared key authentication [#11426](https://github.com/penpot/penpot/issues/11426) (PR: [#11534](https://github.com/penpot/penpot/pull/11534)) - Fix crash when pressing Ctrl+D with no shape selected [#11448](https://github.com/penpot/penpot/issues/11448) (PR: [#11491](https://github.com/penpot/penpot/pull/11491)) - Fix text layout not updating when auto-width is set by double-clicking the bounding box [#11480](https://github.com/penpot/penpot/issues/11480) (PR: [#11541](https://github.com/penpot/penpot/pull/11541)) -- Fix boolean shapes rendering deformed in the WASM renderer and exports [#11482](https://github.com/penpot/penpot/issues/11482) (PR: [#11551](https://github.com/penpot/penpot/pull/11551)) +- Fix workspace crash with index out of bounds when removing multiple interactions rapidly [#11546](https://github.com/penpot/penpot/issues/11546) (PR: [#11621](https://github.com/penpot/penpot/pull/11621)) +- Fix workspace crash with 'too much recursion' error when editing grid layout [#11562](https://github.com/penpot/penpot/issues/11562) (PR: [#11563](https://github.com/penpot/penpot/pull/11563)) +- Fix plugin API error when removing interactions [#11618](https://github.com/penpot/penpot/issues/11618) (PR: [#11621](https://github.com/penpot/penpot/pull/11621)) +- Fix chunked upload storing duplicate objects when the same chunk index is sent twice [#11634](https://github.com/penpot/penpot/issues/11634) (PR: [#11635](https://github.com/penpot/penpot/pull/11635)) +- Fix access token expiration select always showing Never after picking another option (by @chulgil) [#11663](https://github.com/penpot/penpot/issues/11663) (PR: [#11664](https://github.com/penpot/penpot/pull/11664)) +- Fix workspace dropping unsaved changes when read-only blocks persistence [#11672](https://github.com/penpot/penpot/issues/11672) (PR: [#11699](https://github.com/penpot/penpot/pull/11699)) ### :sparkles: New features & Enhancements From e07bda4fe28f8ca6da68b00d20c5188543f4abe9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 15 Sep 2026 19:56:37 +0200 Subject: [PATCH 5/6] :paperclip: Backport .agents from develop --- .agents/skills/create-pr/SKILL.md | 15 ++- .agents/skills/update-changelog/SKILL.md | 136 +++++++++++++++++++++-- 2 files changed, 139 insertions(+), 12 deletions(-) diff --git a/.agents/skills/create-pr/SKILL.md b/.agents/skills/create-pr/SKILL.md index 703609c9a1..f285574bb8 100644 --- a/.agents/skills/create-pr/SKILL.md +++ b/.agents/skills/create-pr/SKILL.md @@ -75,10 +75,23 @@ description structure, writing principles) and `mem:workflow/creating-commits` (commit type emojis). Derive the title and body from the commits and, when there is one, from the issue body. Reference the issue with `Closes #NNNN`. +Repeat the `AI-assisted-by:` trailer in the body, once per model that worked +on the branch, so the PR states the assistance where a reviewer reads it. The +branch commits keep their own trailers, and a squash merge carries every one +of them into the landed message. + +Before offering or accepting a draft PR, warn that CI doesn't run on them. Add +`--draft` only when the user agrees to that. + ```bash -gh pr create --repo penpot/penpot --title "" --body-file /tmp/pr-body.md +gh pr create --repo penpot/penpot --base "<BASE>" --title "<TITLE>" \ + --project "Main" --body-file /tmp/pr-body.md ``` +`--base` is the branch resolved in step 1: without it the PR opens against the +repository default, which is wrong for a branch cut from `staging`. `--project +"Main"` is required by `mem:workflow/creating-prs`. + ### 5. Report Report the PR URL and stop. diff --git a/.agents/skills/update-changelog/SKILL.md b/.agents/skills/update-changelog/SKILL.md index d482959f94..5fa2883d6f 100644 --- a/.agents/skills/update-changelog/SKILL.md +++ b/.agents/skills/update-changelog/SKILL.md @@ -357,6 +357,39 @@ Insert the new version section right after the `# CHANGELOG` header (before the previous version entry). Use the `edit` tool with enough context to make a unique match. +### 8b. Propose and populate the `:rocket: Epics and highlights` subsection + +After inserting the version section, proactively create or populate the +`### :rocket: Epics and highlights` subsection. This section surfaces the +most impactful changes for self-hosted users checking for updates. + +**When to create:** If the version section does not already have a +`### :rocket: Epics and highlights` subsection, create one. Place it before +`### :sparkles:` (matching existing order in CHANGES.md). + +**How to identify highlights:** Review the `:sparkles:` entries for the +version and select 2–5 of the most impactful/user-visible ones. Criteria: +- New user-visible features (not internal refactors) +- Significant capability additions +- Items that create "FOMO" for self-hosted users on older versions + +**Use release notes as hints:** Check +`frontend/src/app/main/ui/releases/v2_<MINOR>.cljs` for the corresponding +version. The slide titles and feature descriptions there are curated +marketing content indicating what the team considers highlight-worthy. Match +those themes to changelog entries. Treat these files as optional hints — they +may not exist for every version. + +**Format requirement:** Every `:rocket:` entry MUST follow the standard +changelog format with issue/PR references: +``` +- <description> [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>)) +``` +An entry without issue AND PR references is a highlight gap (warning, not an anomaly). + +**Preserve existing entries:** If the `:rocket:` section already exists from +a prior run, preserve its entries. Do not remove or rewrite them. + ### 9. Verify Read the top of `CHANGES.md` and confirm: @@ -468,9 +501,8 @@ 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: +PR.** There are two anomaly types, plus two highlight gaps (warnings that +do not count toward the anomaly total): 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 @@ -486,6 +518,13 @@ There are exactly two types: PR that closes an issue with no milestone references an issue from another (probably private) project; that is expected and the issue is not part of this changelog. Do not report it. +3. **missing-highlights (gap):** A released X.Y.0 version section has no + `### :rocket: Epics and highlights` subsection. Patches (X.Y.Z) never + carry highlights, so only minors/majors are checked. +4. **missing-highlight-reference (gap):** A `:rocket:` entry lacks the + required issue AND PR references. Every highlight entry must follow the + standard changelog format with `[#ISSUE]` and `(PR: [#PR])` links + (multi-PR `(PR: [#A](...), [#B](...))` accepted). **Anything else is not an anomaly.** Other discrepancies (exclusion labels on in-changelog issues, missing valid issues, unmerged PR @@ -653,6 +692,40 @@ for pr_num in sorted(changelog_prs): 'issue_milestone': issue_ms, # may be None }) +# --- Type C: released X.Y.0 version sections without :rocket: subsection --- +# Patches (X.Y.Z with Z != 0) never carry :rocket: by design — only minors/majors (X.Y.0). +anomalies_c = [] # list of version strings +rocket_heading_re = re.compile(r'^### :rocket:', re.MULTILINE) +version_sections = re.split(r'(?=^## \d+\.\d+\.\d+)', content, flags=re.MULTILINE) +for vs in version_sections: + m = re.match(r'^## (\d+\.\d+\.\d+)(.*)', vs) + if not m: continue + ver, suffix = m.group(1), m.group(2) + if 'unreleased' in suffix.lower(): continue + if ver.split('.')[2] != '0': continue + if not rocket_heading_re.search(vs): + anomalies_c.append(ver) + +# --- Type D: :rocket: entries without issue AND PR references --- +# Both are required: `[#ISSUE](.../issues/N)` and `(PR: [#PR](.../pull/M))`. +# Multi-PR entries `(PR: [#A](...), [#B](...))` are accepted. +anomalies_d = [] # list of dicts: {version, line} +issue_ref_re = re.compile(r'\[#\d+\]\(https://github\.com/penpot/penpot/issues/\d+\)') +pr_ref_re = re.compile(r'\(PR:\s*\[#\d+\]\(https://github\.com/penpot/penpot/pull/\d+\)(\s*,\s*\[#\d+\]\(https://github\.com/penpot/penpot/pull/\d+\))*\)') +for vs in version_sections: + m = re.match(r'^## (\d+\.\d+\.\d+)(.*)', vs) + if not m: continue + ver = m.group(1) + rocket_match = rocket_heading_re.search(vs) + if not rocket_match: continue + # Extract the :rocket: subsection body (up to next ### or ##) + rocket_body = vs[rocket_match.end():] + rocket_body = re.split(r'(?m)^#{2,3}\s', rocket_body)[0] + for line in rocket_body.splitlines(): + line = line.strip() + if line.startswith('- ') and not (issue_ref_re.search(line) and pr_ref_re.search(line)): + anomalies_d.append({'version': ver, 'line': line[:100]}) + # --- Write report --- def fmt_ms(ms): return ms if ms else "_none_" @@ -664,13 +737,17 @@ with open(OUTPUT, 'w') as f: n_a = len(anomalies_a) n_b = len(anomalies_b) + n_c = len(anomalies_c) + n_d = len(anomalies_d) 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 a different milestone:** {n_b}\n') - f.write(f'- **Total anomalies:** {n_a + n_b}\n\n') + f.write(f'- **Total anomalies:** {n_a + n_b}\n') + f.write(f'- **Released X.Y.0 version missing :rocket: section (gap):** {n_c}\n') + f.write(f'- **:rocket: entry without issue AND PR references (gap):** {n_d}\n\n') - # --- Anomalies section --- + # --- Anomalies section (milestone mismatches only) --- if n_a or n_b: f.write('## Anomalies\n\n') f.write('These are milestone mismatches between an issue in the changelog ' @@ -709,9 +786,37 @@ with open(OUTPUT, 'w') as f: 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') + # --- Highlight gaps (warnings, not anomalies) --- + if n_c or n_d: + f.write('## Highlight gaps\n\n') + f.write('These are warnings, not anomalies: they do not affect the ' + 'milestone-mismatch total above. They track `:rocket:` coverage ' + 'across all released X.Y.0 versions. Historical entries (e.g. ' + 'Taiga links) predate the current reference convention and are ' + 'expected to appear here.\n\n') + + if n_c: + f.write(f'### Released X.Y.0 version missing :rocket: section\n\n') + f.write('These released minors/majors have no `### :rocket: Epics and highlights` subsection. ' + 'Add highlights to help self-hosted users understand what they are missing.\n\n') + for ver in anomalies_c: + f.write(f'- Version **{ver}**\n') + f.write('\n') + + if n_d: + f.write(f'### :rocket: entry without issue AND PR references\n\n') + f.write('These highlight entries lack the required issue AND PR references. ' + 'Add `[#ISSUE](...)` and `(PR: [#PR](...))` links.\n\n') + for d in anomalies_d: + f.write(f'- **{d["version"]}**: `{d["line"]}`\n') + f.write('\n') + elif not (n_a or n_b): + f.write('✅ No highlight gaps found. All released X.Y.0 versions have properly referenced :rocket: entries.\n\n') + # --- Context --- f.write('---\n\n') f.write('## Context\n\n') @@ -726,8 +831,7 @@ print(f"Anomaly report written to {OUTPUT}") PYEOF ``` -This generates `CHANGES-ISSUES.md` containing **only the anomalies** — -milestone mismatches between issues and their referenced PRs: +This generates `CHANGES-ISSUES.md` containing anomalies and highlight gaps: 1. **Issue in milestone, referenced PR in different milestone or no milestone** — the changelog claims a fix here, but the PR is released elsewhere. @@ -736,6 +840,13 @@ milestone mismatches between issues and their referenced PRs: (An issue with *no* milestone belongs to another, probably private, project — milestones are only required on the "Main" project — so it is neither an anomaly nor a changelog candidate.) +3. **missing-highlights (gap, warning)** — a released X.Y.0 version section + has no `### :rocket: Epics and highlights` subsection. Patches (X.Y.Z) + never carry highlights. +4. **missing-highlight-reference (gap, warning)** — a `:rocket:` entry lacks + the required issue AND PR references. + +Gaps do not count toward the anomaly total. **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). @@ -809,10 +920,13 @@ self-contained and clickable in any Markdown viewer. 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. An +- **Anomaly = milestone mismatch only; gaps are warnings.** The report's + anomaly total counts 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. `:rocket:` highlight gaps (missing section on a + released X.Y.0, entry without issue AND PR references) are reported in a + separate `Highlight gaps` section and never count toward the anomaly total. An *unassigned* (milestone-less) issue closed by a milestone PR is **not** an anomaly: milestones are required only for the "Main" project, so such issues come from another (probably private) project and are not changelog From 8ff99f0766a232f9666baa94c8e85e2048169fc6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh <niwi@niwi.nz> Date: Tue, 15 Sep 2026 17:01:22 +0000 Subject: [PATCH 6/6] :books: Document how to add issues as sub-issues Add the verified REST procedure for linking an issue as a sub-issue of an umbrella/EPIC: get the REST id, POST to the parent's sub_issues endpoint with a typed -F field, and verify both directions. Route it from the create-issue skill. AI-assisted-by: deepseek-v4.1-flash --- .agents/skills/create-issue/SKILL.md | 3 ++ .serena/memories/workflow/creating-issues.md | 41 ++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/.agents/skills/create-issue/SKILL.md b/.agents/skills/create-issue/SKILL.md index 90a0fc3b75..8a4e5dde0f 100644 --- a/.agents/skills/create-issue/SKILL.md +++ b/.agents/skills/create-issue/SKILL.md @@ -19,6 +19,9 @@ right flow. - **Create from draft body** — Taiga story, user report, discussion; no PR yet. → memory section **Creating Issues from Draft Body** +- **Create as sub-issue** — the issue must be grouped under an umbrella/EPIC + issue; create it first, then link it to its parent. + → memory section **Adding an Issue as a Sub-issue** - **Retitle existing issue** — current title is vague, prefixed, or stale. → memory section **Retitling an Existing Issue** diff --git a/.serena/memories/workflow/creating-issues.md b/.serena/memories/workflow/creating-issues.md index 54a2993fa8..f698c5031c 100644 --- a/.serena/memories/workflow/creating-issues.md +++ b/.serena/memories/workflow/creating-issues.md @@ -157,6 +157,47 @@ query { repository(owner: "penpot", name: "penpot") { rm -f /tmp/issue-body.md ``` +## Adding an Issue as a Sub-issue + +Sub-issues group work under an umbrella/EPIC issue. `gh issue create` cannot +link a sub-issue at creation time: create the issue first (normal flow above), +then link it. + +**1. Create the sub-issue** as usual and note its number (`NNNN`). + +**2. Get the issue's database id** (the REST `id`, not the `number`): + +```bash +SUB_ID=$(gh api repos/penpot/penpot/issues/NNNN --jq .id) +``` + +**3. Link it to the parent** (`PARENT` = umbrella/EPIC issue number): + +```bash +gh api --method POST repos/penpot/penpot/issues/PARENT/sub_issues \ + -F sub_issue_id=$SUB_ID +``` + +Use `-F` (typed field), never `-f`: with `-f` the value is sent as a string +and the API rejects it with `422 ... /sub_issue_id ... is not of type integer`. + +**4. Verify both directions:** + +```bash +gh api repos/penpot/penpot/issues/NNNN/parent --jq '{number, title}' +gh api repos/penpot/penpot/issues/PARENT/sub_issues --jq '.[] | {number, title}' +``` + +Notes: + +- The `POST` response is the parent issue and includes `sub_issues_summary` + with `total`, `completed` and `percent_completed`, useful to track EPIC + progress. +- A sub-issue has a single parent. +- Issue Type is independent of the parent relationship: choose it with the + normal mapping above (an EPIC child that fixes broken behavior is a Bug, + not a Task). + ## Creating Issues from PRs Used when the project board needs an issue as the primary changelog/release