📎 Backport .agents from develop

This commit is contained in:
Andrey Antukh 2026-09-15 19:56:37 +02:00
parent 02e573100a
commit e07bda4fe2
2 changed files with 139 additions and 12 deletions

View File

@ -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 (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`. 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 ```bash
gh pr create --repo penpot/penpot --title "<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 ### 5. Report
Report the PR URL and stop. Report the PR URL and stop.

View File

@ -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 25 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