From 4ad137aef39f3f850c576754ffbcb8a354f476b1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 18 May 2026 19:50:42 +0200 Subject: [PATCH 1/3] :paperclip: Update gh-issue-from-pr opencode skill --- .opencode/skills/gh-issue-from-pr/SKILL.md | 10 +- tools/gh.py | 370 +++++++++++++++++++++ 2 files changed, 375 insertions(+), 5 deletions(-) create mode 100755 tools/gh.py diff --git a/.opencode/skills/gh-issue-from-pr/SKILL.md b/.opencode/skills/gh-issue-from-pr/SKILL.md index 93f17345f2..012d58f7be 100644 --- a/.opencode/skills/gh-issue-from-pr/SKILL.md +++ b/.opencode/skills/gh-issue-from-pr/SKILL.md @@ -172,19 +172,19 @@ query { repository(owner: "penpot", name: "penpot") { ### 8. Link the PR to the issue -Append `Fixes #` to the PR body: +Append `Closes #` to the PR body: ```bash gh pr view --repo penpot/penpot --json body --jq '.body' > /tmp/pr-body.md -printf "\n\nFixes #\n" >> /tmp/pr-body.md +printf "\n\nCloses #\n" >> /tmp/pr-body.md gh pr edit --repo penpot/penpot --body-file /tmp/pr-body.md # Verify gh pr view --repo penpot/penpot --json body \ - --jq '.body | test("Fixes #")' + --jq '.body | test("Closes #")' ``` -**Note:** If the PR is already merged, `Fixes` won't auto-close the issue +**Note:** If the PR is already merged, `Closes` won't auto-close the issue — it only creates the "Development" sidebar link. This is the desired behavior since the issue is a tracking artifact. @@ -222,7 +222,7 @@ rm -f /tmp/issue-body.md /tmp/pr-body.md - **Copy the milestone from the PR.** Don't guess based on branch names. If the PR has no milestone, create the issue without one. - **Set Issue Type via GraphQL** — `gh issue create` can't set it. -- **Link via PR body** — `Fixes #` creates the "Development" +- **Link via PR body** — `Closes #` creates the "Development" sidebar link automatically. - **One issue per PR** — even if a PR fixes multiple things, create a single issue that summarizes the overall change. diff --git a/tools/gh.py b/tools/gh.py new file mode 100755 index 0000000000..2aa7ac4da8 --- /dev/null +++ b/tools/gh.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +""" +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 + prs Fetch details for one or more PRs + +Usage: + python3 tools/gh.py issues (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" --compare CHANGES.md + 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 + +Prerequisites: + - gh CLI authenticated (gh auth status) + - Python 3.8+ +""" + +import argparse +import json +import re +import subprocess +import sys +from typing import Any + + +REPO = "penpot/penpot" +OWNER = "penpot" +REPO_NAME = "penpot" + + +# ───────────────────────────────────────────── +# Shared helpers +# ───────────────────────────────────────────── + + +def run_gh(method: str, endpoint: str, **kwargs: Any) -> Any: + """Run a ``gh api`` call and return parsed JSON.""" + cmd = ["gh", "api", endpoint, "--method", method] + for key, val in kwargs.items(): + if val is not None: + cmd.extend(["-f", f"{key}={val}"]) + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"gh error: {result.stderr}", file=sys.stderr) + sys.exit(1) + return json.loads(result.stdout) + + +def run_gh_graphql(query: str, variables: dict) -> Any: + """Run a GraphQL query via ``gh api graphql --input -``.""" + payload = json.dumps({"query": query, "variables": variables}) + cmd = ["gh", "api", "graphql", "--input", "-"] + result = subprocess.run(cmd, input=payload, capture_output=True, text=True) + if result.returncode != 0: + print(f"gh error: {result.stderr}", file=sys.stderr) + sys.exit(1) + body = json.loads(result.stdout) + if "errors" in body: + for err in body["errors"]: + print(f"GraphQL error: {err.get('message')}", file=sys.stderr) + sys.exit(1) + return body["data"] + + +# ───────────────────────────────────────────── +# Subcommand: issues +# ───────────────────────────────────────────── + +GQL_ISSUES_QUERY = """\ +query($owner: String!, $repo: String!, $milestone: Int!, $cursor: String) { + repository(owner: $owner, name: $repo) { + milestone(number: $milestone) { + issues(first: 100, after: $cursor, states: __STATES__) { + totalCount + pageInfo { hasNextPage endCursor } + nodes { + ... on Issue { + number + title + state + labels(first: 20) { nodes { name } } + closedByPullRequestsReferences(first: 5) { nodes { number } } + } + } + } + } + } +} +""" + + +def find_milestone(title: str) -> dict: + """Look up milestone by title, return {number, title, open_issues, closed_issues}.""" + data = run_gh("GET", f"repos/{OWNER}/{REPO_NAME}/milestones?per_page=100&state=all") + for ms in data: + if ms["title"] == title: + return { + "number": ms["number"], + "title": ms["title"], + "open_issues": ms["open_issues"], + "closed_issues": ms["closed_issues"], + } + print(f"ERROR: Milestone \"{title}\" not found in {REPO}", file=sys.stderr) + sys.exit(1) + + +def fetch_milestone_issues(milestone_num: int, states: str) -> list[dict]: + """ + Fetch all issues in a milestone via paginated GraphQL. + + Args: + milestone_num: milestone number + states: GraphQL states enum array literal, e.g. ``"[CLOSED]"`` or ``"[OPEN CLOSED]"`` + + Returns: + List of {number, title, state, labels: [str], closing_prs: [int]} + """ + query = GQL_ISSUES_QUERY.replace("__STATES__", states) + all_nodes: list[dict] = [] + cursor: str | None = None + + while True: + variables: dict[str, Any] = { + "owner": OWNER, + "repo": REPO_NAME, + "milestone": milestone_num, + "cursor": cursor, + } + data = run_gh_graphql(query, variables) + issues = data["repository"]["milestone"]["issues"] + page_info = issues["pageInfo"] + + for node in issues["nodes"]: + if node is None: + continue + all_nodes.append({ + "number": node["number"], + "title": node["title"], + "state": node["state"], + "labels": [lbl["name"] for lbl in node["labels"]["nodes"]], + "closing_prs": [pr["number"] for pr in node["closedByPullRequestsReferences"]["nodes"]], + }) + + 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 load_existing_issue_numbers(filepath: str) -> set[int]: + """Parse all ``#NNNN`` references from a file (e.g. CHANGES.md).""" + pattern = re.compile(r"#(\d{3,5})\b") + nums: set[int] = set() + with open(filepath) as f: + for line in f: + for m in pattern.finditer(line): + nums.add(int(m.group(1))) + return nums + + +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) + + # Filter by excluded labels + if args.exclude: + exclusions = set(label.strip() for label in args.exclude.split(",")) + filtered = [issue for issue in issues + if not any(lbl in exclusions for lbl in issue["labels"])] + print(f"After excluding labels: {len(filtered)} issues", file=sys.stderr) + issues = filtered + + # Filter to issues NOT yet in the comparison file (if --compare given) + if args.compare: + existing_nums = load_existing_issue_numbers(args.compare) + missing = [iss for iss in issues if iss["number"] not in existing_nums] + missing.sort(key=lambda x: x["number"]) + print(f"Issues not yet in changelog: {len(missing)}", file=sys.stderr) + issues = missing + + print(json.dumps(issues, indent=2)) + + +# ───────────────────────────────────────────── +# Subcommand: prs +# ───────────────────────────────────────────── + +PRS_BATCH_SIZE = 50 + +GQL_PRS_QUERY_ITEM = """\ + pr_{num}: pullRequest(number: {num}) {{ + number + title + body + state + mergedAt + createdAt + author {{ login }} + labels(first: 20) {{ nodes {{ name }} }} + closingIssuesReferences(first: 5) {{ nodes {{ number }} }} + }} +""" + +GQL_PRS_QUERY_WRAPPER = """\ +query($owner: String!, $repo: String!) {{ + repository(owner: $owner, name: $repo) {{ +{items} + }} +}} +""" + + +def fetch_prs_batch(pr_numbers: list[int]) -> list[dict]: + """ + Fetch details for a list of PR numbers in a single GraphQL query. + + Uses numbered aliases (pr_1234, pr_5678, …) so each PR is looked up by + number in one round-trip. Returns entries in the same order as the input. + """ + items = "\n".join( + GQL_PRS_QUERY_ITEM.format(num=n) for n in pr_numbers + ) + query = GQL_PRS_QUERY_WRAPPER.format(items=items) + variables = {"owner": OWNER, "repo": REPO_NAME} + + data = run_gh_graphql(query, variables) + repo = data["repository"] + + results: list[dict] = [] + for num in pr_numbers: + pr = repo.get(f"pr_{num}") + if pr is None: + results.append({ + "number": num, + "error": "not_found", + }) + continue + results.append({ + "number": pr["number"], + "title": pr["title"], + "body": pr.get("body"), + "state": pr["state"], + "merged_at": pr.get("mergedAt"), + "created_at": pr.get("createdAt"), + "author": pr["author"]["login"] if pr["author"] else None, + "labels": [lbl["name"] for lbl in pr["labels"]["nodes"]], + "closing_issues": [iss["number"] for iss in pr["closingIssuesReferences"]["nodes"]], + }) + return results + + +def cmd_prs(args: argparse.Namespace) -> None: + """Handle the ``prs`` subcommand.""" + + # Collect PR numbers from args / file / stdin + pr_numbers: list[int] = [] + + if args.numbers: + pr_numbers.extend(args.numbers) + + if args.file: + with open(args.file) as f: + for line in f: + line = line.strip() + if line: + pr_numbers.append(int(line)) + + if args.stdin: + for line in sys.stdin: + line = line.strip() + if line: + pr_numbers.append(int(line)) + + if not pr_numbers: + print("ERROR: no PR numbers provided (pass numbers, --file, or --stdin)", + file=sys.stderr) + sys.exit(1) + + # Deduplicate while preserving order + seen: set[int] = set() + pr_numbers = [n for n in pr_numbers if not (n in seen or seen.add(n))] + + print(f"Fetching {len(pr_numbers)} PRs in batches of {PRS_BATCH_SIZE}...", + file=sys.stderr) + + all_results: list[dict] = [] + for i in range(0, len(pr_numbers), PRS_BATCH_SIZE): + batch = pr_numbers[i : i + PRS_BATCH_SIZE] + print(f" batch {i // PRS_BATCH_SIZE + 1}: PRs {batch[0]}..{batch[-1]}", + file=sys.stderr) + all_results.extend(fetch_prs_batch(batch)) + + print(json.dumps(all_results, indent=2)) + + +# ───────────────────────────────────────────── +# CLI entrypoint +# ───────────────────────────────────────────── + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Multi-purpose CLI helper for penpot/penpot GitHub operations" + ) + 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.add_argument( + "--state", choices=["open", "closed", "all"], default="closed", + help="Issue state filter (default: closed)" + ) + p_issues.add_argument( + "--exclude", "--exclude-labels", + help="Comma-separated labels to exclude, e.g. 'release blocker,no changelog'" + ) + p_issues.add_argument( + "--compare", + help="Path to CHANGES.md; only show issues NOT yet referenced in that file" + ) + p_issues.set_defaults(func=cmd_issues) + + # --- prs --- + p_prs = sub.add_parser("prs", help="Fetch details for one or more PRs") + p_prs.add_argument( + "numbers", type=int, nargs="*", + help="PR numbers to fetch (space-separated)" + ) + p_prs.add_argument( + "--file", type=str, + help="File with one PR number per line" + ) + p_prs.add_argument( + "--stdin", action="store_true", + help="Read PR numbers from stdin (one per line)" + ) + p_prs.set_defaults(func=cmd_prs) + + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() From 1161a163a73566f89cc3ff5407020a957a08d07c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 18 May 2026 19:58:00 +0200 Subject: [PATCH 2/3] :arrow_up: Update root repo opencode dependency --- package.json | 2 +- pnpm-lock.yaml | 108 ++++++++++++++++++++++---------------------- pnpm-workspace.yaml | 2 + 3 files changed, 58 insertions(+), 54 deletions(-) create mode 100644 pnpm-workspace.yaml diff --git a/package.json b/package.json index 67031cc6ef..543592511c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,6 @@ "@types/node": "^25.6.2", "esbuild": "^0.28.0", "nrepl-client": "^0.3.0", - "opencode-ai": "^1.14.46" + "opencode-ai": "^1.15.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f019e4a575..aa7c8ea720 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: ^0.3.0 version: 0.3.0 opencode-ai: - specifier: ^1.14.46 - version: 1.14.46 + specifier: ^1.15.4 + version: 1.15.4 packages: @@ -236,67 +236,69 @@ packages: nrepl-client@0.3.0: resolution: {integrity: sha512-EcROXUrzlGHKOdu/E/5WB0OESCI0iGHhdXeYk9cULYtd72eFJrM/Q1umvjTBfKWlT62y76cnyLG/3CmSCqT12w==} - opencode-ai@1.14.46: - resolution: {integrity: sha512-BX3xG3B/t85LpaVEZilbszOfP5aXc4C9e8oRBLX3rPsnnPXweKQZYfz9pn8/kfOz+8rOYuXGrBpJ86UxH/Le+Q==} + opencode-ai@1.15.4: + resolution: {integrity: sha512-Z5PeJwFNUW4sFW+jYHQTnJm6858dECvWOATvnG0S66Nn46zwjaaZJEJMKQEPOW7Yog99j6k7xRKvJPPAjKDXfQ==} + cpu: [arm64, x64] + os: [darwin, linux, win32] hasBin: true - opencode-darwin-arm64@1.14.46: - resolution: {integrity: sha512-KFUZxvEjYHz4cecbbHKJ3utc61iq4GYPjGDEM/GZ9x885FV0xQwhjQXVSQgd1gy/bj/3tFWzVLho3GC6Ogy03A==} + opencode-darwin-arm64@1.15.4: + resolution: {integrity: sha512-d+0sFAAhrDtjQbxRZvYSzy3g/xj/xUDRWUVBWGfkJAx0QEWc/v7cksmnYj3p3l88Gxm/rWeLCh6H32pw1/En8A==} cpu: [arm64] os: [darwin] - opencode-darwin-x64-baseline@1.14.46: - resolution: {integrity: sha512-3OZgCnPZZ19fI1Ju0EHTM7kHPIwadD58OBFSLhCAdJLH00OGRpbpoqxy24gbCm9LnompFjp2DnEQ+KA6+RFIeQ==} + opencode-darwin-x64-baseline@1.15.4: + resolution: {integrity: sha512-Lj9wsEPFyEOgLO6J3DsCXdSu/IAJnW/RjtD1oojAao6uHvhs5uXyj1mrsmK8GrtAfCT4JUh8W38o3YYXGjItSw==} cpu: [x64] os: [darwin] - opencode-darwin-x64@1.14.46: - resolution: {integrity: sha512-+cC9EoVeoU2yLWIq7835QQginkTE8S7rrjMRJk543ZZ+S0yHsrnhEQVkgqhX+Yzncb8Subt2wISF7yROAzq7zA==} + opencode-darwin-x64@1.15.4: + resolution: {integrity: sha512-H412BUw5O+bmXfzLo6UMCWVc3DOYEM0RCI5Kt+Ynqh+Q9878bXK6mwRR7VXgGVBkkH2U4GtT1uDgY0BzSK185Q==} cpu: [x64] os: [darwin] - opencode-linux-arm64-musl@1.14.46: - resolution: {integrity: sha512-2DmmX4WuqrKdEo/rxK/ARA39fCecco53F0/v8t7gk+njbe5fWrKYrfbtL3OaGr8LUsQolny5O05I0tjBsGcTUQ==} + opencode-linux-arm64-musl@1.15.4: + resolution: {integrity: sha512-TO2IVSoYolGKJahf73/hRsJBGxLKOdP/akYPzI0hQQvW4oVrmQkZ3s13jU1+LXIEn4Zbj/TB18QvLzvXrnrEhA==} cpu: [arm64] os: [linux] - opencode-linux-arm64@1.14.46: - resolution: {integrity: sha512-gahGbcQNJ6CAXWeG06wW/U2S+GB2ccmhvp4bAXDZeRm6Liw7M4NzOZX9H7sboTGtcYTnVouBPvoKPaX59qv9rA==} + opencode-linux-arm64@1.15.4: + resolution: {integrity: sha512-V+x/u9JnPOLPEfqbePSCL0OQdin5gs1V35VsVxj19WaZDEwxlMVjOe6HjVKEY64/O6htkPxCCZohmnMU4dVBMQ==} cpu: [arm64] os: [linux] - opencode-linux-x64-baseline-musl@1.14.46: - resolution: {integrity: sha512-lMJiPsb8b+aGjqXdAmfu9f5HyTAS6Cfk8O1GieZFu06pi8kO9oiJ6wPyQwwL8IM6J2ssNF4PxloHgqeNQI7kEg==} + opencode-linux-x64-baseline-musl@1.15.4: + resolution: {integrity: sha512-xOJ3aHg2+2GrT9F/KmAF0JLB1D6K3SCY/626n+fLjs/AEFvLdmE3TYhoXPEyGH2I9F4kF+4p2xk0pg2b+LVlZQ==} cpu: [x64] os: [linux] - opencode-linux-x64-baseline@1.14.46: - resolution: {integrity: sha512-oq6Px+0epCwk2nZn54EUDtxccGb9JlSnF1stAm4oKUeKg/G76hGEbCgg4x3TNiIrpA8QnS9grRlh1oFYIl/efg==} + opencode-linux-x64-baseline@1.15.4: + resolution: {integrity: sha512-dTlV8tAVN8nFdPb7527GR6/BpyIVavAcXJmZ2VbS1daXu4C6k6bpmjiS/ZFKlphRZiKKiEzFrHlimao4BMchVQ==} cpu: [x64] os: [linux] - opencode-linux-x64-musl@1.14.46: - resolution: {integrity: sha512-rUmQHsrlIWcLBmefiom71DWhRzQ0oSqjgvF2Xvg5ySs0GKklRzMB/HcS2ccIydqv02ImL94FbqVVXSZ2lnw4IQ==} + opencode-linux-x64-musl@1.15.4: + resolution: {integrity: sha512-IbMaM6zrakdtDD55GUhlT/WeXomXmKsVqo3XQuOaGXprBg3W5alsxXh60SZpV3ftbdcMD/eiB/PYtN/ZN8Fa5w==} cpu: [x64] os: [linux] - opencode-linux-x64@1.14.46: - resolution: {integrity: sha512-qxFHxbyP3dCFX6vibX71JfVntEbzE7rSMmot6EwbdDB0vq+tcahFEZ+/KVC7qV3zL3ZbDAMsx8OiZknI7Cscmw==} + opencode-linux-x64@1.15.4: + resolution: {integrity: sha512-2c20aldKLfNkg6N6nABvvK1fuaCwYLo/HNeL8ikellkFMeGalCGDhkL/VQ8R8KPV3ohVZJtZwG0nkFiA8MeHCg==} cpu: [x64] os: [linux] - opencode-windows-arm64@1.14.46: - resolution: {integrity: sha512-SmeKzxFNiiF9s9l0lj1CxIZesE+m0VvBX5GuW5CHAlVqNLjmQeW3X8qhHmXelvm9ujGxAudjmXBUlZTkBkH9FQ==} + opencode-windows-arm64@1.15.4: + resolution: {integrity: sha512-kr3nIWmYH7NC0Vgrhgjp9EmCuy5MuxjIRrSjzlfRLMaML6U/a0Hsr3AahBwI1KjT+HEhz5u6xpodZeeEDY3nPQ==} cpu: [arm64] os: [win32] - opencode-windows-x64-baseline@1.14.46: - resolution: {integrity: sha512-6rehvrK+KCqUUKBKFnjpk6YchpiH3TNJdOYTmg+QcRirynyhNjs83F7h29vr+WbSnImMCPYFeRfBAf51yLiAmQ==} + opencode-windows-x64-baseline@1.15.4: + resolution: {integrity: sha512-2/elQ163r4Q97bYJRrY09IG+bpqh0AKpfutDGCaokFdLWIWQN/cFvjzb4C+BKzLFsU9LRfoyvPhe4nXMm1+S4A==} cpu: [x64] os: [win32] - opencode-windows-x64@1.14.46: - resolution: {integrity: sha512-m9LPvvNV9UvgYc5AbzR+hBBD2wPx+bitjbBTXNx/7s+WE/5DhlaHsxVQPePrA9bZ8XAO0EndRnDjvCrLLrkxWA==} + opencode-windows-x64@1.15.4: + resolution: {integrity: sha512-f6p40u3yLEbiq4pzBOXAwtW/NP/dL8uTurHfraPcfezA4ua5DEm4vSoSePUY0CHtubUPuDe0wRUA1s53sysjPQ==} cpu: [x64] os: [win32] @@ -454,55 +456,55 @@ snapshots: bencode: 2.0.3 tree-kill: 1.2.2 - opencode-ai@1.14.46: + opencode-ai@1.15.4: optionalDependencies: - opencode-darwin-arm64: 1.14.46 - opencode-darwin-x64: 1.14.46 - opencode-darwin-x64-baseline: 1.14.46 - opencode-linux-arm64: 1.14.46 - opencode-linux-arm64-musl: 1.14.46 - opencode-linux-x64: 1.14.46 - opencode-linux-x64-baseline: 1.14.46 - opencode-linux-x64-baseline-musl: 1.14.46 - opencode-linux-x64-musl: 1.14.46 - opencode-windows-arm64: 1.14.46 - opencode-windows-x64: 1.14.46 - opencode-windows-x64-baseline: 1.14.46 + opencode-darwin-arm64: 1.15.4 + opencode-darwin-x64: 1.15.4 + opencode-darwin-x64-baseline: 1.15.4 + opencode-linux-arm64: 1.15.4 + opencode-linux-arm64-musl: 1.15.4 + opencode-linux-x64: 1.15.4 + opencode-linux-x64-baseline: 1.15.4 + opencode-linux-x64-baseline-musl: 1.15.4 + opencode-linux-x64-musl: 1.15.4 + opencode-windows-arm64: 1.15.4 + opencode-windows-x64: 1.15.4 + opencode-windows-x64-baseline: 1.15.4 - opencode-darwin-arm64@1.14.46: + opencode-darwin-arm64@1.15.4: optional: true - opencode-darwin-x64-baseline@1.14.46: + opencode-darwin-x64-baseline@1.15.4: optional: true - opencode-darwin-x64@1.14.46: + opencode-darwin-x64@1.15.4: optional: true - opencode-linux-arm64-musl@1.14.46: + opencode-linux-arm64-musl@1.15.4: optional: true - opencode-linux-arm64@1.14.46: + opencode-linux-arm64@1.15.4: optional: true - opencode-linux-x64-baseline-musl@1.14.46: + opencode-linux-x64-baseline-musl@1.15.4: optional: true - opencode-linux-x64-baseline@1.14.46: + opencode-linux-x64-baseline@1.15.4: optional: true - opencode-linux-x64-musl@1.14.46: + opencode-linux-x64-musl@1.15.4: optional: true - opencode-linux-x64@1.14.46: + opencode-linux-x64@1.15.4: optional: true - opencode-windows-arm64@1.14.46: + opencode-windows-arm64@1.15.4: optional: true - opencode-windows-x64-baseline@1.14.46: + opencode-windows-x64-baseline@1.15.4: optional: true - opencode-windows-x64@1.14.46: + opencode-windows-x64@1.15.4: optional: true tree-kill@1.2.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..ca93fb1e8d --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + opencode-ai: true From 87b969bd050557116c2510ff7c3e0e905b48def6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 18 May 2026 19:58:27 +0200 Subject: [PATCH 3/3] :paperclip: Update changelog --- CHANGES.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c34b2ade88..d1a6f7562c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,10 @@ ### :bug: Bugs fixed -- Emit `create-shape-layout` for flex/grid layout creation from plugins and MCP (same event as workspace) [Github #9652](https://github.com/penpot/penpot/issues/9652) +- Emit `create-shape-layout` for flex/grid layout creation from plugins and MCP (same event as workspace) [#9652](https://github.com/penpot/penpot/issues/9652) (PR: [#9654](https://github.com/penpot/penpot/pull/9654)) +- Fix broken authentication on /assets handlers [#9677](https://github.com/penpot/penpot/issues/9677) (PR: [#9679](https://github.com/penpot/penpot/pull/9679)) +- Fix API doc endpoint returning HTML as text/plain [#9680](https://github.com/penpot/penpot/issues/9680) (PR: [#9681](https://github.com/penpot/penpot/pull/9681)) +- Fix unexpected error when opening the export dialog [#9721](https://github.com/penpot/penpot/issues/9721) (PR: [#9704](https://github.com/penpot/penpot/pull/9704)) ## 2.15.3