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