mirror of
https://github.com/penpot/penpot.git
synced 2026-06-09 17:02:05 +00:00
⏪ Backport serena memory and other minor config fixes from develop
This commit is contained in:
parent
11a8d08f95
commit
e0a44eede0
@ -9,7 +9,7 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
|
||||
|
||||
# Development workflow
|
||||
|
||||
- Commit only when explicitly asked. Commit/PR format + changelog: `mem:workflow/creating-commits`, `mem:workflow/creating-prs`.
|
||||
- Commit only when explicitly asked. Commit/PR format + changelog: `mem:workflow/creating-commits`, `mem:workflow/creating-prs`. Issue creation (titles, labels, body templates, Issue Types): `mem:workflow/creating-issues`.
|
||||
- You have access to the GitHub CLI `gh` or corresponding MCP tools.
|
||||
- Issues are also managed on Taiga. Read issues using the `read_taiga_issue` tool.
|
||||
- Before writing code, analyze the task in depth and describe your plan. If the task is complex, break it down into atomic steps.
|
||||
|
||||
160
.serena/memories/workflow/creating-issues.md
Normal file
160
.serena/memories/workflow/creating-issues.md
Normal file
@ -0,0 +1,160 @@
|
||||
# Creating Issues
|
||||
|
||||
Create GitHub issues only on explicit request. Use `gh` CLI authenticated to `penpot/penpot`.
|
||||
|
||||
## Title Derivation
|
||||
|
||||
Derive the title from the source material (bug report, user feedback, feature request, etc.) — not from any pre-existing title which may be auto-generated or stale.
|
||||
|
||||
### Bug titles (descriptive present tense)
|
||||
|
||||
Describe the symptom as it appears to the user. Format: `[Where] [present-tense verb] when [condition]`.
|
||||
|
||||
- *"Plugin API crashes when setting text fills"*
|
||||
- *"Canvas renders glitches when zooming quickly"*
|
||||
- *"French Canada locale falls back to French (fr) translations"*
|
||||
|
||||
Do **not** start bug titles with "Fix" or any imperative verb — state what's broken, not command a fix.
|
||||
|
||||
### Feature / Enhancement titles (imperative mood)
|
||||
|
||||
Command what should be built. Format: `[Imperative verb] [what] in/on [where]`.
|
||||
|
||||
- *"Add customizable dash and gap length controls to dashed strokes in the sidebar"*
|
||||
- *"Show user, timestamp, and hash in the workspace history panel like git commits"*
|
||||
|
||||
### Universal rules
|
||||
|
||||
- **Include the "where"** — specify the UI location or module (e.g. "in the sidebar", "on the stroke options")
|
||||
- **No prefixes** — strip `bug:`, `feature:`, `feat:`, `:bug:`, `:sparkles:`, `[PENPOT FEEDBACK]`, etc.
|
||||
- **No emoji** — plain text only
|
||||
- **Be specific** — prefer concrete detail over generality
|
||||
- **Two problems → cover both** — if the description has two distinct but related issues, capture both joined by "and"
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Rule |
|
||||
|-------|------|
|
||||
| **Labels** | `bug` (crashes/regressions) · `enhancement` (new features) · `community contribution` (PRs from non-core) · skip workflow labels (`backport candidate`, `team-qa`) |
|
||||
| **Milestone** | Use the current or next planned milestone. Fetch available milestones: `gh api repos/penpot/penpot/milestones --jq '.[].title'`. If unsure, omit. |
|
||||
| **Project** | Always `Main` (project number 8). Use `--project "Main"` flag. |
|
||||
| **Issue Type** | See Issue Type section below. Cannot be set via `gh issue create` — use GraphQL after creation. |
|
||||
|
||||
## Issue Body Template
|
||||
|
||||
Write the body to a temp file to avoid shell quoting issues:
|
||||
|
||||
**Bug template:**
|
||||
```markdown
|
||||
### Description
|
||||
|
||||
<what breaks, what the user experiences>
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
1. <step 1>
|
||||
2. <step 2>
|
||||
|
||||
### Expected behavior
|
||||
|
||||
<what should happen instead>
|
||||
|
||||
### Affected versions
|
||||
|
||||
<version>
|
||||
```
|
||||
|
||||
**Enhancement template:**
|
||||
```markdown
|
||||
### Description
|
||||
|
||||
<what the user can now do that they couldn't before>
|
||||
|
||||
### Use case
|
||||
|
||||
<why this is useful, who benefits>
|
||||
|
||||
### Affected versions
|
||||
|
||||
<version>
|
||||
```
|
||||
|
||||
## Creating the Issue
|
||||
|
||||
```bash
|
||||
cat > /tmp/issue-body.md << 'ISSUE_BODY'
|
||||
<body content here>
|
||||
ISSUE_BODY
|
||||
|
||||
gh issue create \
|
||||
--repo penpot/penpot \
|
||||
--title "<Derived title>" \
|
||||
--label "<label>" \
|
||||
--project "Main" \
|
||||
--body-file /tmp/issue-body.md
|
||||
```
|
||||
|
||||
Output: `https://github.com/penpot/penpot/issues/<NUMBER>`
|
||||
|
||||
## Setting the Issue Type
|
||||
|
||||
`gh issue create` can't set Issue Type directly. Use GraphQL after creation.
|
||||
|
||||
**Issue Type IDs for penpot/penpot:**
|
||||
|
||||
| Type | ID |
|
||||
|------|----|
|
||||
| Bug | `IT_kwDOAcyBPM4AX5Nb` |
|
||||
| Enhancement | `IT_kwDOAcyBPM4B_IQN` |
|
||||
| Feature | `IT_kwDOAcyBPM4AX5Nf` |
|
||||
| Task | `IT_kwDOAcyBPM4AX5NY` |
|
||||
| Question | `IT_kwDOAcyBPM4B_IQj` |
|
||||
| Docs | `IT_kwDOAcyBPM4B_IQz` |
|
||||
|
||||
**Map:**
|
||||
- `bug` label → Bug
|
||||
- `enhancement` label → Enhancement
|
||||
- Feature/epic → Feature
|
||||
- Docs → Docs
|
||||
- None of the above → Task
|
||||
|
||||
**Set it:**
|
||||
```bash
|
||||
ISSUE_ID=$(gh api graphql -f query='
|
||||
query { repository(owner: "penpot", name: "penpot") {
|
||||
issue(number: <NUMBER>) { id }
|
||||
}}' --jq '.data.repository.issue.id')
|
||||
|
||||
gh api graphql -f query='
|
||||
mutation {
|
||||
updateIssue(input: {
|
||||
id: "'"$ISSUE_ID"'"
|
||||
issueTypeId: "<TYPE_ID>"
|
||||
}) {
|
||||
issue { number issueType { name } }
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo penpot/penpot \
|
||||
--json title,labels,milestone,projectItems \
|
||||
--jq '{title, milestone: .milestone.title, labels: [.labels[].name], projects: [.projectItems[].title]}'
|
||||
|
||||
gh api graphql -f query='
|
||||
query { repository(owner: "penpot", name: "penpot") {
|
||||
issue(number: <NUMBER>) { issueType { name } }
|
||||
}}' --jq '.data.repository.issue.issueType.name'
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
rm -f /tmp/issue-body.md
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- Creating issues **from PRs** (separating WHAT from HOW): `mem:workflow/creating-prs`
|
||||
@ -11,6 +11,10 @@
|
||||
:asset-path "/js"
|
||||
:devtools {:watch-dir "resources/public"
|
||||
:reload-strategy :full}
|
||||
|
||||
:dev {;; allows remote-relay per parallel environment
|
||||
;; inside :dev so the integration tests won't use it
|
||||
:devtools {:devtools-url #shadow/env ["SHADOW_SERVER_URL" :default ""]}}
|
||||
:build-options {:manifest-name "manifest.json"}
|
||||
:modules
|
||||
{:shared
|
||||
@ -86,9 +90,12 @@
|
||||
{:target :browser
|
||||
:output-dir "resources/public/js/worker/"
|
||||
:asset-path "/js/worker"
|
||||
:devtools {:browser-inject :main
|
||||
:watch-dir "resources/public"
|
||||
:reload-strategy :full}
|
||||
:devtools {:watch-dir "resources/public"
|
||||
:reload-strategy :full
|
||||
:browser-inject :main}
|
||||
:dev {;; allows remote-relay per parallel environment
|
||||
;; inside :dev so the integration tests won't use it
|
||||
:devtools {:devtools-url #shadow/env ["SHADOW_SERVER_URL" :default ""]}}
|
||||
:build-options {:manifest-name "manifest.json"}
|
||||
:modules
|
||||
{:main
|
||||
|
||||
156
tools/gh.py
156
tools/gh.py
@ -5,18 +5,23 @@ gh.py — Multi-purpose CLI helper for penpot/penpot GitHub operations.
|
||||
Uses GitHub GraphQL and REST APIs via the authenticated ``gh`` CLI.
|
||||
|
||||
Subcommands:
|
||||
issues List issues in a milestone
|
||||
issues List issues in a milestone (or unassigned with milestone=none)
|
||||
prs Fetch details for one or more PRs (by number or milestone)
|
||||
|
||||
Usage:
|
||||
python3 tools/gh.py issues <milestone-title> (default: state=closed)
|
||||
python3 tools/gh.py issues <milestone-title> (default: state=closed)
|
||||
python3 tools/gh.py issues "2.16.0" --state all
|
||||
python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
|
||||
python3 tools/gh.py issues "2.16.0" --label "bug" (include only issues with label)
|
||||
python3 tools/gh.py issues "2.16.0" --label "bug,regression" --exclude "no changelog"
|
||||
python3 tools/gh.py issues "2.16.0" --compare CHANGES.md
|
||||
python3 tools/gh.py issues none (issues with no milestone)
|
||||
python3 tools/gh.py issues none --label "enhancement"
|
||||
python3 tools/gh.py issues none --state open
|
||||
python3 tools/gh.py prs 9179 9204 9311
|
||||
python3 tools/gh.py prs --file prs.txt
|
||||
cat prs.txt | python3 tools/gh.py prs --stdin
|
||||
python3 tools/gh.py prs --milestone "2.16.0" (default: state=merged)
|
||||
python3 tools/gh.py prs --milestone "2.16.0" (default: state=merged)
|
||||
python3 tools/gh.py prs --milestone "2.16.0" --state all
|
||||
|
||||
Prerequisites:
|
||||
@ -134,6 +139,110 @@ query($owner: String!, $repo: String!, $milestone: Int!, $cursor: String) {
|
||||
"""
|
||||
|
||||
|
||||
GQL_NO_MILESTONE_QUERY = """\
|
||||
query($query: String!, $cursor: String) {
|
||||
search(
|
||||
query: $query
|
||||
type: ISSUE
|
||||
first: 100
|
||||
after: $cursor
|
||||
) {
|
||||
issueCount
|
||||
pageInfo { hasNextPage endCursor }
|
||||
nodes {
|
||||
... on Issue {
|
||||
number
|
||||
title
|
||||
state
|
||||
milestone { title }
|
||||
issueType { name }
|
||||
labels(first: 20) { nodes { name } }
|
||||
closedByPullRequestsReferences(first: 5) { nodes { number } }
|
||||
projectItems(first: 10) {
|
||||
nodes {
|
||||
project { title }
|
||||
fieldValueByName(name: "Status") {
|
||||
... on ProjectV2ItemFieldSingleSelectValue {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def fetch_no_milestone_issues(states: str, labels: str | None = None) -> list[dict]:
|
||||
"""
|
||||
Fetch all issues that belong to NO milestone via paginated GraphQL search.
|
||||
|
||||
Args:
|
||||
states: GraphQL states enum array literal, e.g. ``"[CLOSED]"`` or ``"[OPEN CLOSED]"``
|
||||
labels: optional comma-separated labels to include (built into the search query)
|
||||
|
||||
Returns:
|
||||
List of {number, title, state, milestone, issue_type, labels, closing_prs, project_status}
|
||||
"""
|
||||
all_nodes: list[dict] = []
|
||||
cursor: str | None = None
|
||||
|
||||
# Map states enum literal to search qualifiers
|
||||
state_qualifiers = {
|
||||
"[OPEN]": "is:open",
|
||||
"[CLOSED]": "is:closed",
|
||||
"[OPEN CLOSED]": "",
|
||||
}
|
||||
state_q = state_qualifiers.get(states, "")
|
||||
label_q = ""
|
||||
if labels:
|
||||
for lbl in labels.split(","):
|
||||
label_q += f" label:\"{lbl.strip()}\""
|
||||
search_query = f"repo:{OWNER}/{REPO_NAME} is:issue no:milestone{state_q}{label_q}".strip()
|
||||
while True:
|
||||
variables: dict[str, Any] = {
|
||||
"query": search_query,
|
||||
"cursor": cursor,
|
||||
}
|
||||
data = run_gh_graphql(GQL_NO_MILESTONE_QUERY, variables)
|
||||
search = data["search"]
|
||||
page_info = search["pageInfo"]
|
||||
|
||||
for node in search["nodes"]:
|
||||
if node is None:
|
||||
continue
|
||||
issue_type = node.get("issueType")
|
||||
ms = node.get("milestone")
|
||||
project_status = None
|
||||
for pi in (node.get("projectItems") or {}).get("nodes") or []:
|
||||
project = pi.get("project") or {}
|
||||
if project.get("title") == "Main":
|
||||
status_field = pi.get("fieldValueByName") or {}
|
||||
project_status = status_field.get("name")
|
||||
break
|
||||
all_nodes.append({
|
||||
"number": node["number"],
|
||||
"title": node["title"],
|
||||
"state": node["state"],
|
||||
"milestone": ms["title"] if ms else None,
|
||||
"issue_type": issue_type["name"] if issue_type else None,
|
||||
"labels": [lbl["name"] for lbl in node["labels"]["nodes"]],
|
||||
"closing_prs": [pr["number"] for pr in node["closedByPullRequestsReferences"]["nodes"]],
|
||||
"project_status": project_status,
|
||||
})
|
||||
|
||||
total = len(all_nodes)
|
||||
print(f" ... fetched {total} issues so far", file=sys.stderr)
|
||||
|
||||
if not page_info["hasNextPage"]:
|
||||
break
|
||||
cursor = page_info["endCursor"]
|
||||
|
||||
return all_nodes
|
||||
|
||||
|
||||
def fetch_milestone_issues(milestone_num: int, states: str) -> list[dict]:
|
||||
"""
|
||||
Fetch all issues in a milestone via paginated GraphQL.
|
||||
@ -206,20 +315,25 @@ def load_existing_issue_numbers(filepath: str) -> set[int]:
|
||||
def cmd_issues(args: argparse.Namespace) -> None:
|
||||
"""Handle the ``issues`` subcommand."""
|
||||
|
||||
# Resolve milestone
|
||||
print(f"Looking up milestone \"{args.milestone}\"...", file=sys.stderr)
|
||||
ms = find_milestone(args.milestone)
|
||||
print(f"Milestone #{ms['number']}: {ms['open_issues']} open, {ms['closed_issues']} closed",
|
||||
file=sys.stderr)
|
||||
|
||||
# Map state to GraphQL enum array literal
|
||||
state_map = {"open": "[OPEN]", "closed": "[CLOSED]", "all": "[OPEN CLOSED]"}
|
||||
gql_states = state_map[args.state]
|
||||
|
||||
# Fetch issues
|
||||
print(f"Fetching {args.state} issues via GraphQL...", file=sys.stderr)
|
||||
issues = fetch_milestone_issues(ms["number"], gql_states)
|
||||
print(f"Fetched {len(issues)} issues total", file=sys.stderr)
|
||||
# ── No-milestone path ──────────────────────────────────────────
|
||||
if args.milestone and args.milestone.lower() == "none":
|
||||
print("Fetching issues with NO milestone...", file=sys.stderr)
|
||||
issues = fetch_no_milestone_issues(gql_states, labels=args.label)
|
||||
print(f"Fetched {len(issues)} issues total", file=sys.stderr)
|
||||
|
||||
# ── Milestone path ─────────────────────────────────────────────
|
||||
else:
|
||||
print(f"Looking up milestone \"{args.milestone}\"...", file=sys.stderr)
|
||||
ms = find_milestone(args.milestone)
|
||||
print(f"Milestone #{ms['number']}: {ms['open_issues']} open, {ms['closed_issues']} closed",
|
||||
file=sys.stderr)
|
||||
print(f"Fetching {args.state} issues via GraphQL...", file=sys.stderr)
|
||||
issues = fetch_milestone_issues(ms["number"], gql_states)
|
||||
print(f"Fetched {len(issues)} issues total", file=sys.stderr)
|
||||
|
||||
# Filter by excluded labels
|
||||
if args.exclude:
|
||||
@ -229,6 +343,14 @@ def cmd_issues(args: argparse.Namespace) -> None:
|
||||
print(f"After excluding labels: {len(filtered)} issues", file=sys.stderr)
|
||||
issues = filtered
|
||||
|
||||
# Filter by included labels (--label) — issue must have ALL specified labels
|
||||
if args.label:
|
||||
inclusions = set(label.strip() for label in args.label.split(","))
|
||||
filtered = [issue for issue in issues
|
||||
if all(lbl in issue["labels"] for lbl in inclusions)]
|
||||
print(f"After filtering by labels: {len(filtered)} issues", file=sys.stderr)
|
||||
issues = filtered
|
||||
|
||||
# Filter out issues with "Rejected" project status (unless --include-rejected)
|
||||
if not args.include_rejected:
|
||||
rejected = [iss for iss in issues if iss.get("project_status") == "Rejected"]
|
||||
@ -464,8 +586,8 @@ def main() -> None:
|
||||
sub = parser.add_subparsers(dest="command", required=True, title="subcommands")
|
||||
|
||||
# --- issues ---
|
||||
p_issues = sub.add_parser("issues", help="List issues in a milestone")
|
||||
p_issues.add_argument("milestone", help="Milestone title, e.g. '2.16.0'")
|
||||
p_issues = sub.add_parser("issues", help="List issues in a milestone (or use 'none' for unassigned)")
|
||||
p_issues.add_argument("milestone", help="Milestone title (e.g. '2.16.0') or 'none' for issues with no milestone")
|
||||
p_issues.add_argument(
|
||||
"--state", choices=["open", "closed", "all"], default="closed",
|
||||
help="Issue state filter (default: closed)"
|
||||
@ -474,6 +596,10 @@ def main() -> None:
|
||||
"--exclude", "--exclude-labels",
|
||||
help="Comma-separated labels to exclude, e.g. 'release blocker,no changelog'"
|
||||
)
|
||||
p_issues.add_argument(
|
||||
"--label", "--labels",
|
||||
help="Comma-separated labels to include (issue must have ALL specified), e.g. 'bug' or 'bug,regression'"
|
||||
)
|
||||
p_issues.add_argument(
|
||||
"--compare",
|
||||
help="Path to CHANGES.md; only show issues NOT yet referenced in that file"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user