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_.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: +``` +- [#](https://github.com/penpot/penpot/issues/) (PR: [#](https://github.com/penpot/penpot/pull/)) +``` +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 diff --git a/.gitignore b/.gitignore index 722f0e219d..3ed13ac9e3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ opencode.json !AGENTS.md !CODE_OF_CONDUCT.md !SECURITY.md -!HIGHLIGHTS.md /*.png /*.svg /*.sql diff --git a/CHANGES.md b/CHANGES.md index 1666868044..9db0d25976 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -218,6 +218,10 @@ ### :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)) +- Add layer blur effect for visual depth and styling [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034)) +- Render guides in WebGL for consistent viewer performance [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014)) +- Add concurrency limiter and status indicators for MCP server communications [#9493](https://github.com/penpot/penpot/issues/9493) (PR: [#9748](https://github.com/penpot/penpot/pull/9748)) +- Add typography token row to multiselected texts for better token visibility [#9336](https://github.com/penpot/penpot/issues/9336) (PR: [#9128](https://github.com/penpot/penpot/pull/9128)) ### :sparkles: New features & Enhancements @@ -572,6 +576,10 @@ ## 2.15.0 +### :rocket: Epics and highlights + +- Add MCP server integration for AI-assisted design workflows [#9174](https://github.com/penpot/penpot/issues/9174) (PR: [#9032](https://github.com/penpot/penpot/pull/9032), [#9321](https://github.com/penpot/penpot/pull/9321)) + ### :sparkles: New features & Enhancements - Add MCP server integration [GH #9174](https://github.com/penpot/penpot/issues/9174) diff --git a/HIGHLIGHTS.md b/HIGHLIGHTS.md deleted file mode 100644 index e66b4b4497..0000000000 --- a/HIGHLIGHTS.md +++ /dev/null @@ -1,26 +0,0 @@ -# HIGHLIGHTS - -## 2.17.0 - -- Background blur is here -- WebGL rendering gets stronger -- MCP connection status and more -- Design tokens: more visible, more user-friendly - - -## 2.16.0 - -- Design tokens in the design panel -- Major community contributions -- WebGL rendering (beta) - - -## 2.15.0 - -- AI connected to real design context -- Multi-directional workflow -- Your stack, your model, your decision - - - - diff --git a/frontend/src/app/main/ui/dashboard/check_updates.cljs b/frontend/src/app/main/ui/dashboard/check_updates.cljs index 407b6b434d..8bf5f72786 100644 --- a/frontend/src/app/main/ui/dashboard/check_updates.cljs +++ b/frontend/src/app/main/ui/dashboard/check_updates.cljs @@ -29,11 +29,11 @@ (def ^:private telemetry-origin "check-updates-modal") -(def ^:private highlights-md-url - "https://raw.githubusercontent.com/penpot/penpot/refs/heads/staging/HIGHLIGHTS.md") +(def ^:private changelog-md-url + "https://raw.githubusercontent.com/penpot/penpot/refs/heads/staging/CHANGES.md") (def ^:private changelog-url - "https://github.com/penpot/penpot/blob/staging/CHANGES.md") + "https://github.com/penpot/penpot/blob/main/CHANGES.md") (def ^:private release-notes-url "https://penpot.app/release-notes") @@ -44,6 +44,9 @@ (def ^:private bullet-re #"^- (.+)$") +(def ^:private rocket-heading-re + #"(?m)^### :rocket: Epics and highlights\s*$") + (defn- unreleased-suffix? [suffix] (str/includes? (str/lower (or suffix "")) "unreleased")) @@ -56,9 +59,54 @@ item))) vec)) +(defn- extract-rocket-items + "Given a version section body, find the :rocket: subsection and + extract its bullet items. Returns nil if no :rocket: or empty." + [version-body] + (when-let [[_ rocket-body] (str/split version-body rocket-heading-re 2)] + (let [subsection (-> (str/split rocket-body #"(?m)(?=^#{2,3}\s)") first)] + (when subsection + (let [items (parse-section-items subsection)] + (when (seq items) items)))))) + +(def ^:private inline-md-re + #"\[([^\]]+)\]\(((?:\([^)]*\)|[^)\s])*)\)|\*\*([^*]+)\*\*") + +(defn- http-url? + [url] + (boolean (re-matches #"https?://.*" (or url "")))) + +(defn parse-highlight-item + "Parse one highlight string with inline markdown (links, bold) into a + vector of {:type :text/:link/:bold, :text ..., :href ...} descriptors. + Anything unrecognized degrades to :text. Total over strings; nil and + empty input return []." + [text] + (if (or (not (string? text)) (= "" text)) + [] + (loop [out [] s text] + (if (= "" s) + out + (if-let [[m link-text link-href bold-text] (re-find inline-md-re s)] + (let [idx (cstr/index-of s m) + before (subs s 0 idx) + after (subs s (+ idx (count m))) + out (if (= "" before) out (conj out {:type :text :text before}))] + (cond + (some? link-text) + (if (http-url? link-href) + (recur (conj out {:type :link :text link-text :href link-href}) after) + (recur (conj out {:type :text :text m}) after)) + + :else + (recur (conj out {:type :bold :text bold-text}) after))) + (conj out {:type :text :text s})))))) + (defn parse-highlights - "Parse HIGHLIGHTS.md into released version sections with bullet items. - Skips Unreleased headings. Preserves file order (newest first)." + "Parse CHANGES.md into released version sections with bullet items from + the :rocket: Epics and highlights subsection. Skips Unreleased headings, + versions without a :rocket: section, and versions with an empty one. + Preserves file order (newest first)." [markdown] (if-not (string? markdown) [] @@ -66,14 +114,19 @@ (keep (fn [part] (when-let [[_ version suffix] (re-find version-heading-re part)] (when-not (unreleased-suffix? suffix) - {:version version - :items (parse-section-items part)})))) + (when-let [items (extract-rocket-items part)] + {:version version + :items items}))))) vec))) (defn parse-latest-released-version - "Return the first non-unreleased `## X.Y.Z` heading from a highlights body." + "Return the first non-unreleased `## X.Y.Z` heading from a CHANGES.md body." [markdown] - (some-> (parse-highlights markdown) first :version)) + (when (string? markdown) + (some->> (re-seq version-heading-re markdown) + (keep (fn [[_ version suffix]] + (when-not (unreleased-suffix? suffix) version))) + first))) (defn highlights-until-installed "Keep released sections newer than the installed version (major, minor, @@ -101,8 +154,8 @@ (defn- handle-highlights [installed body] - (let [sections (parse-highlights body) - latest (some-> sections first :version)] + (let [latest (parse-latest-released-version body) + sections (parse-highlights body)] (cond (nil? latest) (show-unable-dialog) @@ -124,7 +177,7 @@ (->> (http/send! {:method :get :mode :cors :omit-default-headers true - :uri highlights-md-url + :uri changelog-md-url :response-type :text}) (rx/subs! (fn [response] @@ -280,23 +333,37 @@ :class (stl/css :modal-msg)} (tr "dashboard.check-updates.available-message")] - [:> text* {:as "h3" - :typography t/headline-small - :class (stl/css :highlights-title)} - (tr "dashboard.check-updates.highlights-title")] + (when (seq highlights) + [:* + [:> text* {:as "h3" + :typography t/headline-small + :class (stl/css :highlights-title)} + (tr "dashboard.check-updates.highlights-title")] - [:div {:class (stl/css :highlights-scroll)} - (for [section highlights] - (let [version (:version section) - items (:items section)] - [:div {:key version - :class (stl/css :highlights-section)} - [:div {:class (stl/css :highlights-version)} version] - [:ul {:class (stl/css :highlights-list)} - (for [item items] - [:li {:key item - :class (stl/css :highlights-item)} - item])]]))]] + [:div {:class (stl/css :highlights-scroll)} + (for [section highlights] + (let [version (:version section) + items (:items section)] + [:div {:key version + :class (stl/css :highlights-section)} + [:div {:class (stl/css :highlights-version)} version] + [:ul {:class (stl/css :highlights-list)} + (for [item items] + [:li {:key item + :class (stl/css :highlights-item)} + (for [[idx frag] (map-indexed vector (parse-highlight-item item))] + (case (:type frag) + :link + [:a {:key idx + :href (:href frag) + :target "_blank" + :rel "noopener noreferrer"} + (:text frag)] + + :bold + [:strong {:key idx} (:text frag)] + + (:text frag)))])]]))]])] [:div {:class (stl/css :modal-footer :modal-footer-available)} [:> button* {:variant "secondary" diff --git a/frontend/src/app/main/ui/dashboard/check_updates.scss b/frontend/src/app/main/ui/dashboard/check_updates.scss index 26f5855a09..bfa243033d 100644 --- a/frontend/src/app/main/ui/dashboard/check_updates.scss +++ b/frontend/src/app/main/ui/dashboard/check_updates.scss @@ -162,6 +162,20 @@ border-radius: $br-circle; background-color: var(--color-accent-primary); } + + a { + color: var(--color-accent-primary); + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + strong { + color: var(--color-foreground-primary); + font-weight: 700; + } } .modal-footer { diff --git a/frontend/test/frontend_tests/ui/check_updates_test.cljs b/frontend/test/frontend_tests/ui/check_updates_test.cljs index 7e51105cae..79794e5e37 100644 --- a/frontend/test/frontend_tests/ui/check_updates_test.cljs +++ b/frontend/test/frontend_tests/ui/check_updates_test.cljs @@ -10,30 +10,49 @@ [app.main.ui.dashboard.check-updates :as dcu] [cljs.test :as t :include-macros true])) -(def ^:private sample-highlights - (str "# HIGHLIGHTS\n" +(def ^:private sample-changes + (str "# CHANGELOG\n" "\n" "## 2.18.0 (Unreleased)\n" "\n" - "- To do\n" + "### :sparkles: New features & Enhancements\n" "\n" - "## 2.17.2\n" + "- Something WIP\n" "\n" - "- Background blur is here\n" - "- WebGL rendering gets stronger\n" + "## 2.17.0\n" "\n" - "## 2.17.1\n" + "### :rocket: Epics and highlights\n" "\n" - "- MCP connection status and more\n" - "- Design tokens: more visible, more user-friendly\n")) + "- Background blur [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))\n" + "- WebGL rendering [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014))\n" + "\n" + "### :sparkles: New features & Enhancements\n" + "\n" + "- Other stuff\n" + "\n" + "## 2.16.0\n" + "\n" + "### :rocket: Epics and highlights\n" + "\n" + "### :sparkles: New features & Enhancements\n" + "\n" + "- Tokens stuff\n" + "\n" + "## 2.15.0\n" + "\n" + "### :sparkles: New features & Enhancements\n" + "\n" + "- MCP server\n")) -(t/deftest parse-latest-released-version-skips-unreleased - (t/is (= "2.17.2" (dcu/parse-latest-released-version sample-highlights)))) +;; --- parse-latest-released-version --- + +(t/deftest parse-latest-released-version-from-changes + (t/is (= "2.17.0" (dcu/parse-latest-released-version sample-changes)))) (t/deftest parse-latest-released-version-first-released - (t/is (= "2.17.2" + (t/is (= "2.17.0" (dcu/parse-latest-released-version - "## 2.17.2\n\n- Fix\n\n## 2.17.1\n\n- Fix\n")))) + "## 2.17.0\n\n### :sparkles:\n\n- Fix\n\n## 2.16.0\n\n### :sparkles:\n\n- Fix\n")))) (t/deftest parse-latest-released-version-only-unreleased (t/is (nil? (dcu/parse-latest-released-version @@ -41,30 +60,154 @@ (t/deftest parse-latest-released-version-empty (t/is (nil? (dcu/parse-latest-released-version ""))) - (t/is (nil? (dcu/parse-latest-released-version "# HIGHLIGHTS\n")))) + (t/is (nil? (dcu/parse-latest-released-version "# CHANGELOG\n")))) -(t/deftest parse-highlights-skips-unreleased-and-collects-bullets - (t/is (= [{:version "2.17.2" - :items ["Background blur is here" - "WebGL rendering gets stronger"]} - {:version "2.17.1" - :items ["MCP connection status and more" - "Design tokens: more visible, more user-friendly"]}] - (dcu/parse-highlights sample-highlights)))) +;; --- parse-highlights --- + +(t/deftest parse-highlights-extracts-rocket-items + (let [result (dcu/parse-highlights sample-changes)] + (t/is (= [{:version "2.17.0" + :items ["Background blur [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))" + "WebGL rendering [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014))"]}] + result)))) + +(t/deftest parse-highlights-skips-empty-rocket + ;; 2.16.0 has an empty :rocket: section — should be skipped + (let [result (dcu/parse-highlights sample-changes)] + (t/is (not (some #(= "2.16.0" (:version %)) result))))) + +(t/deftest parse-highlights-skips-missing-rocket + ;; 2.15.0 has no :rocket: section — should be skipped + (let [result (dcu/parse-highlights sample-changes)] + (t/is (not (some #(= "2.15.0" (:version %)) result))))) + +(t/deftest parse-highlights-skips-unreleased + ;; 2.18.0 (Unreleased) should be skipped + (let [result (dcu/parse-highlights sample-changes)] + (t/is (not (some #(= "2.18.0" (:version %)) result))))) + +(t/deftest parse-highlights-empty-input + (t/is (= [] (dcu/parse-highlights ""))) + (t/is (= [] (dcu/parse-highlights nil))) + (t/is (= [] (dcu/parse-highlights 42)))) + +(t/deftest parse-highlights-extracts-multiple-versions + (let [input (str "## 2.17.0\n\n### :rocket: Epics and highlights\n\n" + "- Feature A [#1](https://github.com/penpot/penpot/issues/1)\n\n" + "## 2.16.0\n\n### :rocket: Epics and highlights\n\n" + "- Feature B [#2](https://github.com/penpot/penpot/issues/2)\n") + result (dcu/parse-highlights input)] + (t/is (= 2 (count result))) + (t/is (= "2.17.0" (:version (first result)))) + (t/is (= "2.16.0" (:version (second result)))))) + +;; --- version-compare --- (t/deftest version-compare - (t/is (zero? (v/compare-versions "2.17.1" "2.17.1"))) - (t/is (pos? (v/compare-versions "2.17.2" "2.17.1"))) - (t/is (neg? (v/compare-versions "2.17.1" "2.17.2"))) + (t/is (zero? (v/compare-versions "2.17.0" "2.17.0"))) + (t/is (pos? (v/compare-versions "2.17.0" "2.16.0"))) + (t/is (neg? (v/compare-versions "2.16.0" "2.17.0"))) (t/is (pos? (v/compare-versions "3.0.0" "2.99.99"))) - (t/is (neg? (v/compare-versions "2.17.2" "2.17.10")))) + (t/is (neg? (v/compare-versions "2.17.0" "2.17.10")))) + +;; --- highlights-until-installed --- (t/deftest highlights-until-installed - (let [sections (dcu/parse-highlights sample-highlights)] - (t/is (= [{:version "2.17.2" - :items ["Background blur is here" - "WebGL rendering gets stronger"]}] - (dcu/highlights-until-installed sections "2.17.1"))) - (t/is (= [] (dcu/highlights-until-installed sections "2.17.2"))) - (t/is (= sections (dcu/highlights-until-installed sections "2.16.0"))) - (t/is (= [] (dcu/highlights-until-installed sections "2.17.10"))))) + (let [sections (dcu/parse-highlights sample-changes)] + ;; installed 2.16.0 → shows 2.17.0 highlights + (t/is (= [{:version "2.17.0" + :items ["Background blur [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))" + "WebGL rendering [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014))"]}] + (dcu/highlights-until-installed sections "2.16.0"))) + ;; installed 2.17.0 → no newer highlights + (t/is (= [] (dcu/highlights-until-installed sections "2.17.0"))) + ;; installed older version → shows all available highlights + (t/is (= sections (dcu/highlights-until-installed sections "2.14.0"))) + ;; installed newer than any highlight → empty + (t/is (= [] (dcu/highlights-until-installed sections "2.99.0"))))) + +;; --- parse-highlight-item --- + +(t/deftest parse-highlight-item-link + (t/is (= [{:type :text :text "See "} + {:type :link :text "#9844" + :href "https://github.com/penpot/penpot/issues/9844"}] + (dcu/parse-highlight-item + "See [#9844](https://github.com/penpot/penpot/issues/9844)")))) + +(t/deftest parse-highlight-item-bold + (t/is (= [{:type :bold :text "New plugin system."}] + (dcu/parse-highlight-item "**New plugin system.**")))) + +(t/deftest parse-highlight-item-mixed-real-line + (t/is (= [{:type :text :text "Background blur "} + {:type :link :text "#9844" + :href "https://github.com/penpot/penpot/issues/9844"} + {:type :text :text " (PR: "} + {:type :link :text "#10034" + :href "https://github.com/penpot/penpot/pull/10034"} + {:type :text :text ")"}] + (dcu/parse-highlight-item + "Background blur [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))")))) + +(t/deftest parse-highlight-item-plain-text + (t/is (= [{:type :text :text "Just plain text, no markdown"}] + (dcu/parse-highlight-item "Just plain text, no markdown")))) + +(t/deftest parse-highlight-item-empty + (t/is (= [] (dcu/parse-highlight-item ""))) + (t/is (= [] (dcu/parse-highlight-item nil)))) + +(t/deftest parse-highlight-item-unbalanced-fallback + (t/is (= [{:type :text :text "Broken [link without end"}] + (dcu/parse-highlight-item "Broken [link without end"))) + (t/is (= [{:type :text :text "Unclosed **bold"}] + (dcu/parse-highlight-item "Unclosed **bold")))) + +(t/deftest parse-highlight-item-non-http-url-is-plain-text + (t/is (= [{:type :text :text "[click](javascript:alert(1))"}] + (dcu/parse-highlight-item "[click](javascript:alert(1))")))) + +;; --- parse-highlight-item edge cases --- + +(t/deftest parse-highlight-item-taiga-link + (t/is (= [{:type :text :text "Grid CSS layout "} + {:type :link :text "Taiga #4915" + :href "https://tree.taiga.io/project/penpot/epic/4915"}] + (dcu/parse-highlight-item + "Grid CSS layout [Taiga #4915](https://tree.taiga.io/project/penpot/epic/4915)")))) + +(t/deftest parse-highlight-item-multi-pr-line + (t/is (= [{:type :text :text "Add MCP "} + {:type :link :text "#9174" + :href "https://github.com/penpot/penpot/issues/9174"} + {:type :text :text " (PR: "} + {:type :link :text "#9032" + :href "https://github.com/penpot/penpot/pull/9032"} + {:type :text :text ", "} + {:type :link :text "#9321" + :href "https://github.com/penpot/penpot/pull/9321"} + {:type :text :text ")"}] + (dcu/parse-highlight-item + "Add MCP [#9174](https://github.com/penpot/penpot/issues/9174) (PR: [#9032](https://github.com/penpot/penpot/pull/9032), [#9321](https://github.com/penpot/penpot/pull/9321))")))) + +(t/deftest parse-highlight-item-link-only + (t/is (= [{:type :link :text "#1" + :href "https://github.com/penpot/penpot/issues/1"}] + (dcu/parse-highlight-item + "[#1](https://github.com/penpot/penpot/issues/1)")))) + +(t/deftest parse-highlight-item-bold-adjacent-to-link + (t/is (= [{:type :bold :text "Hot"} + {:type :text :text " "} + {:type :link :text "#1" + :href "https://github.com/penpot/penpot/issues/1"}] + (dcu/parse-highlight-item + "**Hot** [#1](https://github.com/penpot/penpot/issues/1)")))) + +(t/deftest parse-highlight-item-bold-inside-link-is-not-nested + ;; Link wins; inner ** stays raw text (documented, no nesting) + (t/is (= [{:type :link :text "a **b**" + :href "https://github.com/penpot/penpot/issues/1"}] + (dcu/parse-highlight-item + "[a **b**](https://github.com/penpot/penpot/issues/1)"))))