mirror of
https://github.com/penpot/penpot.git
synced 2026-09-12 23:19:28 +00:00
♻️ Consolidate HIGHLIGHTS.md into CHANGES.md 🚀 section (#11531)
* ♻️ Consolidate HIGHLIGHTS.md into CHANGES.md 🚀 section Eliminate the redundant HIGHLIGHTS.md file and make CHANGES.md the single source of truth for version highlights. - Add 🚀 section for 2.15.0 (MCP server integration) - Add 4 missing highlight entries to 2.17.0 🚀 section - Rewrite frontend parser to extract from CHANGES.md 🚀 subsections instead of flat HIGHLIGHTS.md format - Decouple parse-latest-released-version from highlights extraction so it works independently of 🚀 content - Conditionally render highlights section in modal when non-empty - Rewrite tests for new parser behavior (11 tests, 21 assertions) - Delete HIGHLIGHTS.md and remove .gitignore exception - Add step 8b to update-changelog skill for proactively proposing highlights during release workflows - Add missing-highlights and missing-highlight-reference anomaly types to the changelog anomaly report script Closes #11530 AI-assisted-by: qwen3.7-plus * ♻️ Use consistent string library and add multi-version test Address code review findings: - Use str/split (cuerdas) consistently in extract-rocket-items instead of mixing cstr/split (clojure.string) - Add parse-highlights-extracts-multiple-versions test to verify the parser correctly extracts 🚀 items from multiple versions in a single CHANGES.md body AI-assisted-by: qwen3.7-plus * ♻️ Scope 🚀 checks to X.Y.0 and split gaps from anomalies Type C now only checks released X.Y.0 versions, since patches never carry 🚀 subsections by design. Type D requires both issue AND PR references with exact format, accepting multi-PR entries. C/D are reported as highlight gaps in their own section and no longer count toward the anomaly total. Key Principles and anomaly definitions updated to match. Addresses review comments on PR #11531. AI-assisted-by: muse-spark-1.3-contributor * ✨ Render markdown links and bold in check-updates highlights The highlights modal showed raw markdown from CHANGES.md 🚀 lines (brackets and URLs). Add a pure parse-highlight-item parser for inline links and bold, render fragments with literal hiccup in the modal (links open in a new tab), and style links and strong elements. Non-http URLs and malformed markup degrade to plain text. Adds 12 unit tests. AI-assisted-by: muse-spark-1.3-contributor * 🐛 Point full changelog link to main instead of staging The view-changelog button in the check-updates modal linked to the staging branch. Point it to main, which holds the published changelog. Version detection still fetches from staging. AI-assisted-by: muse-spark-1.3-contributor
This commit is contained in:
parent
286ccb03fa
commit
7c27ed812a
@ -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
|
the previous version entry). Use the `edit` tool with enough context to make
|
||||||
a unique match.
|
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
|
### 9. Verify
|
||||||
|
|
||||||
Read the top of `CHANGES.md` and confirm:
|
Read the top of `CHANGES.md` and confirm:
|
||||||
@ -468,9 +501,8 @@ Markdown viewer.
|
|||||||
## What is an anomaly
|
## What is an anomaly
|
||||||
|
|
||||||
**An anomaly is a milestone-mismatch between an issue and its referenced
|
**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,
|
PR.** There are two anomaly types, plus two highlight gaps (warnings that
|
||||||
all in milestone M" is inconsistent with the actual milestone assignments.
|
do not count toward the anomaly total):
|
||||||
There are exactly two types:
|
|
||||||
|
|
||||||
1. **Issue is in the milestone, but its referenced PR is in a different
|
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
|
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
|
PR that closes an issue with no milestone references an issue from
|
||||||
another (probably private) project; that is expected and the issue is
|
another (probably private) project; that is expected and the issue is
|
||||||
not part of this changelog. Do not report it.
|
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
|
**Anything else is not an anomaly.** Other discrepancies (exclusion
|
||||||
labels on in-changelog issues, missing valid issues, unmerged PR
|
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
|
'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 ---
|
# --- Write report ---
|
||||||
def fmt_ms(ms):
|
def fmt_ms(ms):
|
||||||
return ms if ms else "_none_"
|
return ms if ms else "_none_"
|
||||||
@ -664,13 +737,17 @@ with open(OUTPUT, 'w') as f:
|
|||||||
|
|
||||||
n_a = len(anomalies_a)
|
n_a = len(anomalies_a)
|
||||||
n_b = len(anomalies_b)
|
n_b = len(anomalies_b)
|
||||||
|
n_c = len(anomalies_c)
|
||||||
|
n_d = len(anomalies_d)
|
||||||
|
|
||||||
f.write('## Summary\n\n')
|
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'- **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'- **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:
|
if n_a or n_b:
|
||||||
f.write('## Anomalies\n\n')
|
f.write('## Anomalies\n\n')
|
||||||
f.write('These are milestone mismatches between an issue in the changelog '
|
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 '⚠️'
|
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(f' - {badge} Closing {issue_link(e["issue"])} is in milestone **{ms_label}** (expected: {MILESTONE})\n')
|
||||||
f.write('\n')
|
f.write('\n')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
f.write('✅ No anomalies found. All (issue, PR) pairs in the changelog have aligned milestone assignments.\n\n')
|
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 ---
|
# --- Context ---
|
||||||
f.write('---\n\n')
|
f.write('---\n\n')
|
||||||
f.write('## Context\n\n')
|
f.write('## Context\n\n')
|
||||||
@ -726,8 +831,7 @@ print(f"Anomaly report written to {OUTPUT}")
|
|||||||
PYEOF
|
PYEOF
|
||||||
```
|
```
|
||||||
|
|
||||||
This generates `CHANGES-ISSUES.md` containing **only the anomalies** —
|
This generates `CHANGES-ISSUES.md` containing anomalies and highlight gaps:
|
||||||
milestone mismatches between issues and their referenced PRs:
|
|
||||||
|
|
||||||
1. **Issue in milestone, referenced PR in different milestone or no milestone** —
|
1. **Issue in milestone, referenced PR in different milestone or no milestone** —
|
||||||
the changelog claims a fix here, but the PR is released elsewhere.
|
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,
|
(An issue with *no* milestone belongs to another, probably private,
|
||||||
project — milestones are only required on the "Main" project — so it is
|
project — milestones are only required on the "Main" project — so it is
|
||||||
neither an anomaly nor a changelog candidate.)
|
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
|
**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).
|
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
|
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
|
are clearly unrelated, or the PR predates the issue by years, treat it as a
|
||||||
data glitch and skip it.
|
data glitch and skip it.
|
||||||
- **Anomaly = milestone mismatch only.** The report contains only milestone
|
- **Anomaly = milestone mismatch only; gaps are warnings.** The report's
|
||||||
mismatches: (1) the issue is in this milestone but the referenced PR is
|
anomaly total counts only milestone mismatches: (1) the issue is in this
|
||||||
in a different milestone (or unassigned), and (2) the PR is in this
|
milestone but the referenced PR is in a different milestone (or unassigned),
|
||||||
milestone but the issue it closes is in a different milestone. An
|
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**
|
*unassigned* (milestone-less) issue closed by a milestone PR is **not**
|
||||||
an anomaly: milestones are required only for the "Main" project, so such
|
an anomaly: milestones are required only for the "Main" project, so such
|
||||||
issues come from another (probably private) project and are not changelog
|
issues come from another (probably private) project and are not changelog
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -24,7 +24,6 @@ opencode.json
|
|||||||
!AGENTS.md
|
!AGENTS.md
|
||||||
!CODE_OF_CONDUCT.md
|
!CODE_OF_CONDUCT.md
|
||||||
!SECURITY.md
|
!SECURITY.md
|
||||||
!HIGHLIGHTS.md
|
|
||||||
/*.png
|
/*.png
|
||||||
/*.svg
|
/*.svg
|
||||||
/*.sql
|
/*.sql
|
||||||
|
|||||||
@ -218,6 +218,10 @@
|
|||||||
### :rocket: Epics and highlights
|
### :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))
|
- 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
|
### :sparkles: New features & Enhancements
|
||||||
|
|
||||||
@ -572,6 +576,10 @@
|
|||||||
|
|
||||||
## 2.15.0
|
## 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
|
### :sparkles: New features & Enhancements
|
||||||
|
|
||||||
- Add MCP server integration [GH #9174](https://github.com/penpot/penpot/issues/9174)
|
- Add MCP server integration [GH #9174](https://github.com/penpot/penpot/issues/9174)
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -29,11 +29,11 @@
|
|||||||
(def ^:private telemetry-origin
|
(def ^:private telemetry-origin
|
||||||
"check-updates-modal")
|
"check-updates-modal")
|
||||||
|
|
||||||
(def ^:private highlights-md-url
|
(def ^:private changelog-md-url
|
||||||
"https://raw.githubusercontent.com/penpot/penpot/refs/heads/staging/HIGHLIGHTS.md")
|
"https://raw.githubusercontent.com/penpot/penpot/refs/heads/staging/CHANGES.md")
|
||||||
|
|
||||||
(def ^:private changelog-url
|
(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
|
(def ^:private release-notes-url
|
||||||
"https://penpot.app/release-notes")
|
"https://penpot.app/release-notes")
|
||||||
@ -44,6 +44,9 @@
|
|||||||
(def ^:private bullet-re
|
(def ^:private bullet-re
|
||||||
#"^- (.+)$")
|
#"^- (.+)$")
|
||||||
|
|
||||||
|
(def ^:private rocket-heading-re
|
||||||
|
#"(?m)^### :rocket: Epics and highlights\s*$")
|
||||||
|
|
||||||
(defn- unreleased-suffix?
|
(defn- unreleased-suffix?
|
||||||
[suffix]
|
[suffix]
|
||||||
(str/includes? (str/lower (or suffix "")) "unreleased"))
|
(str/includes? (str/lower (or suffix "")) "unreleased"))
|
||||||
@ -56,9 +59,54 @@
|
|||||||
item)))
|
item)))
|
||||||
vec))
|
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
|
(defn parse-highlights
|
||||||
"Parse HIGHLIGHTS.md into released version sections with bullet items.
|
"Parse CHANGES.md into released version sections with bullet items from
|
||||||
Skips Unreleased headings. Preserves file order (newest first)."
|
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]
|
[markdown]
|
||||||
(if-not (string? markdown)
|
(if-not (string? markdown)
|
||||||
[]
|
[]
|
||||||
@ -66,14 +114,19 @@
|
|||||||
(keep (fn [part]
|
(keep (fn [part]
|
||||||
(when-let [[_ version suffix] (re-find version-heading-re part)]
|
(when-let [[_ version suffix] (re-find version-heading-re part)]
|
||||||
(when-not (unreleased-suffix? suffix)
|
(when-not (unreleased-suffix? suffix)
|
||||||
{:version version
|
(when-let [items (extract-rocket-items part)]
|
||||||
:items (parse-section-items part)}))))
|
{:version version
|
||||||
|
:items items})))))
|
||||||
vec)))
|
vec)))
|
||||||
|
|
||||||
(defn parse-latest-released-version
|
(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]
|
[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
|
(defn highlights-until-installed
|
||||||
"Keep released sections newer than the installed version (major, minor,
|
"Keep released sections newer than the installed version (major, minor,
|
||||||
@ -101,8 +154,8 @@
|
|||||||
|
|
||||||
(defn- handle-highlights
|
(defn- handle-highlights
|
||||||
[installed body]
|
[installed body]
|
||||||
(let [sections (parse-highlights body)
|
(let [latest (parse-latest-released-version body)
|
||||||
latest (some-> sections first :version)]
|
sections (parse-highlights body)]
|
||||||
(cond
|
(cond
|
||||||
(nil? latest)
|
(nil? latest)
|
||||||
(show-unable-dialog)
|
(show-unable-dialog)
|
||||||
@ -124,7 +177,7 @@
|
|||||||
(->> (http/send! {:method :get
|
(->> (http/send! {:method :get
|
||||||
:mode :cors
|
:mode :cors
|
||||||
:omit-default-headers true
|
:omit-default-headers true
|
||||||
:uri highlights-md-url
|
:uri changelog-md-url
|
||||||
:response-type :text})
|
:response-type :text})
|
||||||
(rx/subs!
|
(rx/subs!
|
||||||
(fn [response]
|
(fn [response]
|
||||||
@ -280,23 +333,37 @@
|
|||||||
:class (stl/css :modal-msg)}
|
:class (stl/css :modal-msg)}
|
||||||
(tr "dashboard.check-updates.available-message")]
|
(tr "dashboard.check-updates.available-message")]
|
||||||
|
|
||||||
[:> text* {:as "h3"
|
(when (seq highlights)
|
||||||
:typography t/headline-small
|
[:*
|
||||||
:class (stl/css :highlights-title)}
|
[:> text* {:as "h3"
|
||||||
(tr "dashboard.check-updates.highlights-title")]
|
:typography t/headline-small
|
||||||
|
:class (stl/css :highlights-title)}
|
||||||
|
(tr "dashboard.check-updates.highlights-title")]
|
||||||
|
|
||||||
[:div {:class (stl/css :highlights-scroll)}
|
[:div {:class (stl/css :highlights-scroll)}
|
||||||
(for [section highlights]
|
(for [section highlights]
|
||||||
(let [version (:version section)
|
(let [version (:version section)
|
||||||
items (:items section)]
|
items (:items section)]
|
||||||
[:div {:key version
|
[:div {:key version
|
||||||
:class (stl/css :highlights-section)}
|
:class (stl/css :highlights-section)}
|
||||||
[:div {:class (stl/css :highlights-version)} version]
|
[:div {:class (stl/css :highlights-version)} version]
|
||||||
[:ul {:class (stl/css :highlights-list)}
|
[:ul {:class (stl/css :highlights-list)}
|
||||||
(for [item items]
|
(for [item items]
|
||||||
[:li {:key item
|
[:li {:key item
|
||||||
:class (stl/css :highlights-item)}
|
:class (stl/css :highlights-item)}
|
||||||
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)}
|
[:div {:class (stl/css :modal-footer :modal-footer-available)}
|
||||||
[:> button* {:variant "secondary"
|
[:> button* {:variant "secondary"
|
||||||
|
|||||||
@ -162,6 +162,20 @@
|
|||||||
border-radius: $br-circle;
|
border-radius: $br-circle;
|
||||||
background-color: var(--color-accent-primary);
|
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 {
|
.modal-footer {
|
||||||
|
|||||||
@ -10,30 +10,49 @@
|
|||||||
[app.main.ui.dashboard.check-updates :as dcu]
|
[app.main.ui.dashboard.check-updates :as dcu]
|
||||||
[cljs.test :as t :include-macros true]))
|
[cljs.test :as t :include-macros true]))
|
||||||
|
|
||||||
(def ^:private sample-highlights
|
(def ^:private sample-changes
|
||||||
(str "# HIGHLIGHTS\n"
|
(str "# CHANGELOG\n"
|
||||||
"\n"
|
"\n"
|
||||||
"## 2.18.0 (Unreleased)\n"
|
"## 2.18.0 (Unreleased)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"- To do\n"
|
"### :sparkles: New features & Enhancements\n"
|
||||||
"\n"
|
"\n"
|
||||||
"## 2.17.2\n"
|
"- Something WIP\n"
|
||||||
"\n"
|
"\n"
|
||||||
"- Background blur is here\n"
|
"## 2.17.0\n"
|
||||||
"- WebGL rendering gets stronger\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
"## 2.17.1\n"
|
"### :rocket: Epics and highlights\n"
|
||||||
"\n"
|
"\n"
|
||||||
"- MCP connection status and more\n"
|
"- Background blur [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))\n"
|
||||||
"- Design tokens: more visible, more user-friendly\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
|
;; --- parse-latest-released-version ---
|
||||||
(t/is (= "2.17.2" (dcu/parse-latest-released-version sample-highlights))))
|
|
||||||
|
(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/deftest parse-latest-released-version-first-released
|
||||||
(t/is (= "2.17.2"
|
(t/is (= "2.17.0"
|
||||||
(dcu/parse-latest-released-version
|
(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/deftest parse-latest-released-version-only-unreleased
|
||||||
(t/is (nil? (dcu/parse-latest-released-version
|
(t/is (nil? (dcu/parse-latest-released-version
|
||||||
@ -41,30 +60,154 @@
|
|||||||
|
|
||||||
(t/deftest parse-latest-released-version-empty
|
(t/deftest parse-latest-released-version-empty
|
||||||
(t/is (nil? (dcu/parse-latest-released-version "")))
|
(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
|
;; --- parse-highlights ---
|
||||||
(t/is (= [{:version "2.17.2"
|
|
||||||
:items ["Background blur is here"
|
(t/deftest parse-highlights-extracts-rocket-items
|
||||||
"WebGL rendering gets stronger"]}
|
(let [result (dcu/parse-highlights sample-changes)]
|
||||||
{:version "2.17.1"
|
(t/is (= [{:version "2.17.0"
|
||||||
:items ["MCP connection status and more"
|
:items ["Background blur [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))"
|
||||||
"Design tokens: more visible, more user-friendly"]}]
|
"WebGL rendering [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014))"]}]
|
||||||
(dcu/parse-highlights sample-highlights))))
|
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/deftest version-compare
|
||||||
(t/is (zero? (v/compare-versions "2.17.1" "2.17.1")))
|
(t/is (zero? (v/compare-versions "2.17.0" "2.17.0")))
|
||||||
(t/is (pos? (v/compare-versions "2.17.2" "2.17.1")))
|
(t/is (pos? (v/compare-versions "2.17.0" "2.16.0")))
|
||||||
(t/is (neg? (v/compare-versions "2.17.1" "2.17.2")))
|
(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 (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
|
(t/deftest highlights-until-installed
|
||||||
(let [sections (dcu/parse-highlights sample-highlights)]
|
(let [sections (dcu/parse-highlights sample-changes)]
|
||||||
(t/is (= [{:version "2.17.2"
|
;; installed 2.16.0 → shows 2.17.0 highlights
|
||||||
:items ["Background blur is here"
|
(t/is (= [{:version "2.17.0"
|
||||||
"WebGL rendering gets stronger"]}]
|
:items ["Background blur [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))"
|
||||||
(dcu/highlights-until-installed sections "2.17.1")))
|
"WebGL rendering [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014))"]}]
|
||||||
(t/is (= [] (dcu/highlights-until-installed sections "2.17.2")))
|
(dcu/highlights-until-installed sections "2.16.0")))
|
||||||
(t/is (= sections (dcu/highlights-until-installed sections "2.16.0")))
|
;; installed 2.17.0 → no newer highlights
|
||||||
(t/is (= [] (dcu/highlights-until-installed sections "2.17.10")))))
|
(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)"))))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user