mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 02:46:20 +00:00
Compare commits
No commits in common. "develop" and "2.16.0-RC9" have entirely different histories.
develop
...
2.16.0-RC9
@ -1,133 +0,0 @@
|
||||
# `.devenv/` — Per-Workspace AI-Client MCP Configs
|
||||
|
||||
This directory carries the pieces needed to point an AI coding agent
|
||||
(currently Claude Code, opencode, VS Code Copilot, and the OpenAI Codex CLI)
|
||||
at the MCP servers running inside the parallel devenv instance the developer
|
||||
is currently working in. Every parallel workspace (`ws0`, `ws1`, …) has its
|
||||
own copy because the Penpot MCP and Serena MCP host ports are
|
||||
workspace-specific.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
.devenv/
|
||||
README.md
|
||||
scripts/
|
||||
merge-mcp-config.py # generator helper invoked by manage.sh
|
||||
shared/ # committed; workspace-independent entries
|
||||
claude-code.json # Playwright — same for every workspace
|
||||
opencode.json
|
||||
vscode.json
|
||||
codex.toml
|
||||
templates/ # committed; entries with ${...} port placeholders
|
||||
claude-code.json # Penpot MCP, Serena MCP — port is the only diff
|
||||
opencode.json
|
||||
vscode.json
|
||||
codex.toml
|
||||
mcp/ # gitignored; written by manage.sh per workspace
|
||||
claude-code.json # loaded via Claude Code's --mcp-config flag
|
||||
opencode.json # loaded via OPENCODE_CONFIG env var
|
||||
```
|
||||
|
||||
One more file is generated outside `.devenv/`, in the directory VS Code itself
|
||||
auto-discovers (gitignored):
|
||||
|
||||
```
|
||||
.vscode/mcp.json # auto-loaded by GitHub Copilot in VS Code
|
||||
```
|
||||
|
||||
Codex is the exception: it has no way to load an MCP config from an arbitrary
|
||||
path, and its only project-level config file (`.codex/config.toml`) is one a
|
||||
developer may already own. So we do **not** write a file for Codex at all —
|
||||
`start-coding-agent codex` injects our servers as `-c` command-line overrides
|
||||
built fresh from `shared/codex.toml` + `templates/codex.toml` at launch.
|
||||
|
||||
* **`shared/`** holds MCP entries that don't depend on the workspace — the
|
||||
browser-driving Playwright server today, plus any other workspace-independent
|
||||
servers we add later. Same content in every workspace, so it's a static
|
||||
checked-in file.
|
||||
* **`templates/`** holds the workspace-specific entries (Penpot MCP, Serena
|
||||
MCP) with `${PENPOT_MCP_PORT}` and `${SERENA_MCP_PORT}` placeholders. The
|
||||
placeholders are resolved per-workspace from the port-base constants in
|
||||
`manage.sh`.
|
||||
* **`mcp/`** (Claude Code, opencode) is the result of merging `shared/` with
|
||||
the port-substituted `templates/`. `manage.sh` writes these on every
|
||||
`run-devenv --agentic` pass. Gitignored, dedicated paths with no developer
|
||||
content — never edit by hand, your edits will be overwritten on the next
|
||||
reconcile.
|
||||
* **`.vscode/mcp.json`** is the same merge, but written to the path VS Code
|
||||
auto-discovers. Because on `ws0` that path *is* the live repo's own file, the
|
||||
reconcile **deep-merges** into it: any servers you added yourself are kept,
|
||||
and only the entries we manage (`penpot`, `serena-devenv`, `playwright`) are
|
||||
(re)written to the current ports. On `ws1+` the file doesn't exist yet, so it
|
||||
is created from scratch.
|
||||
* **`scripts/merge-mcp-config.py`** is the generator. Its `json` mode does the
|
||||
JSON deep-merge (with `--merge-into-existing` for the VS Code path); its
|
||||
`codex-args` mode prints the `-c` assignments for Codex. `manage.sh`'s
|
||||
`_merge-mcp-config-json` helper is a thin shim over the former, and
|
||||
`start-coding-agent` calls the latter directly. Run
|
||||
`python3 .devenv/scripts/merge-mcp-config.py --help` for the CLI.
|
||||
|
||||
## Launching a coding agent
|
||||
|
||||
The easiest path is the wrapper command, which knows the right flags per
|
||||
client, `cd`'s into the target workspace, and refuses to launch unless the
|
||||
target instance is running and its MCP config has been generated:
|
||||
|
||||
```bash
|
||||
# Default target is ws0 (the live repo).
|
||||
./manage.sh start-coding-agent claude [...args to forward]
|
||||
./manage.sh start-coding-agent opencode [...args to forward]
|
||||
./manage.sh start-coding-agent vscode [...args to forward to 'code']
|
||||
./manage.sh start-coding-agent codex [...args to forward]
|
||||
|
||||
# Target a parallel workspace with --ws N. N is an integer (non-negative);
|
||||
# 'main', 'ws1' and similar spellings are rejected.
|
||||
./manage.sh start-coding-agent claude --ws 1
|
||||
./manage.sh start-coding-agent opencode --ws 2
|
||||
```
|
||||
|
||||
Equivalents by hand (run from inside the workspace directory):
|
||||
|
||||
```bash
|
||||
claude --mcp-config .devenv/mcp/claude-code.json
|
||||
OPENCODE_CONFIG=.devenv/mcp/opencode.json opencode
|
||||
code "$PWD" # VS Code auto-discovers .vscode/mcp.json
|
||||
# Codex: pass our servers as -c overrides (no config file is written).
|
||||
codex $(python3 .devenv/scripts/merge-mcp-config.py --format codex-args \
|
||||
.devenv/shared/codex.toml .devenv/templates/codex.toml \
|
||||
| sed 's/^/-c /')
|
||||
```
|
||||
|
||||
`start-coding-agent codex` does the `-c` wiring for you (and resolves the
|
||||
workspace's ports first). Because our servers arrive as command-line
|
||||
overrides, no "trusted project" prompt is involved for them — that prompt only
|
||||
gates Codex's own `.codex/config.toml`, which we never write.
|
||||
|
||||
## Overriding our entries
|
||||
|
||||
Both the auto-discovered configs and the launcher-loaded configs sit *on top
|
||||
of* the developer's global config (with varying precedence rules). All four
|
||||
clients offer escape hatches for shadowing entries we ship:
|
||||
|
||||
* **Claude Code** — `claude mcp add --scope local …` installs a private entry
|
||||
that overrides the one in `mcp/claude-code.json`. Local scope wins.
|
||||
* **opencode** — drop an `opencode.json` at the repo root with the override
|
||||
entries you need. opencode's precedence chain is *global → `OPENCODE_CONFIG`
|
||||
→ project*, so the project file always wins. The root `opencode.json` is
|
||||
gitignored on purpose, since these overrides are personal.
|
||||
* **VS Code Copilot** — the reconcile deep-merges into `.vscode/mcp.json`, so
|
||||
any servers you add there yourself are preserved (only `penpot`,
|
||||
`serena-devenv` and `playwright` are rewritten). To shadow one of *ours*,
|
||||
put an entry under the same name in your VS Code user-profile MCP config —
|
||||
it is loaded alongside the workspace file and wins.
|
||||
* **Codex CLI** — our servers arrive as `-c` overrides, which are Codex's
|
||||
highest-precedence layer, so they win over a same-named `[mcp_servers.<name>]`
|
||||
in your `~/.codex/config.toml` or a project `.codex/config.toml`. To override
|
||||
one of ours, append your own `-c` after the client name — extra args are
|
||||
forwarded after ours and the later `-c` wins, e.g.
|
||||
`./manage.sh start-coding-agent codex -- -c 'mcp_servers.penpot.url="…"'`.
|
||||
|
||||
See `docs/technical-guide/developer/agentic-devenv.md` for the broader
|
||||
client-configuration story (browser remote debugging, AI-client config
|
||||
schemas, manual setup for unsupported clients).
|
||||
@ -1,204 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Combine a shared MCP-server config with a port-substituted template for one
|
||||
AI coding-agent client.
|
||||
|
||||
Invoked per workspace by manage.sh's `write-instance-mcp-configs` (JSON
|
||||
clients) and by `start-coding-agent` (Codex). Each supported client ships a
|
||||
`.devenv/shared/<tool>.{json,toml}` (workspace-independent entries, e.g.
|
||||
Playwright) and a `.devenv/templates/<tool>.{json,toml}` (per-workspace entries
|
||||
with `${PENPOT_MCP_PORT}` / `${SERENA_MCP_PORT}` placeholders). This script
|
||||
combines the two for the target client.
|
||||
|
||||
Two output modes are supported:
|
||||
|
||||
json Deep-merge two JSON documents under a configurable top-level key
|
||||
(`mcpServers` for Claude Code, `mcp` for opencode, `servers` for
|
||||
VS Code Copilot) and write the result to <out>. Same-name
|
||||
entries in the template override entries in shared. With
|
||||
--merge-into-existing, any pre-existing <out> file is loaded as
|
||||
the lowest-precedence layer first, so entries the developer
|
||||
already had are preserved (ours win on name collision). This is
|
||||
used for VS Code's auto-discovered `.vscode/mcp.json`, which on
|
||||
ws0 IS the live repo's file and may hold the developer's own
|
||||
servers; the Claude/opencode outputs live in a dedicated,
|
||||
gitignored `.devenv/mcp/` path and are written without the flag
|
||||
(a clean overwrite).
|
||||
|
||||
codex-args Deep-merge the two TOML chunks and print one
|
||||
`dotted.key=<toml-value>` assignment per line to stdout (no
|
||||
<out> file). The caller wraps each line in a `codex -c` flag.
|
||||
Codex has no way to load an MCP config from an arbitrary file
|
||||
path (CODEX_HOME would relocate auth/history too), so rather than
|
||||
writing the auto-discovered `.codex/config.toml` we inject our
|
||||
servers as ephemeral per-invocation overrides. This never
|
||||
touches the developer's project- or user-level Codex config.
|
||||
|
||||
In both modes, `${VAR}` placeholders inside *either* chunk are resolved from
|
||||
the current environment (only template chunks carry placeholders in practice,
|
||||
but the substitution is uniform either way) using Python's
|
||||
`os.path.expandvars`. Undefined placeholders are left as `${VAR}` literal text
|
||||
-- callers (i.e. manage.sh) are responsible for exporting the variables before
|
||||
invoking the script.
|
||||
|
||||
Usage:
|
||||
merge-mcp-config.py --format json --key <key> [--merge-into-existing] \
|
||||
<shared> <template> <out>
|
||||
merge-mcp-config.py --format codex-args <shared> <template>
|
||||
|
||||
Exit codes:
|
||||
0 success
|
||||
2 argparse error (missing required option, bad value, unreadable input)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def merge_json(
|
||||
shared_path: Path,
|
||||
tpl_path: Path,
|
||||
out_path: Path,
|
||||
key: str,
|
||||
merge_into_existing: bool,
|
||||
) -> None:
|
||||
"""Deep-merge JSON documents under a single top-level dict key into out.
|
||||
|
||||
Precedence (lowest to highest): an existing <out> file (only when
|
||||
merge_into_existing is set), then shared, then the template. Entries under
|
||||
`key` are merged by name, so the template wins on a name collision while
|
||||
every other entry the lower layers contributed is kept. Top-level keys
|
||||
other than `key` come from the existing file and shared (shared wins).
|
||||
"""
|
||||
shared = json.loads(shared_path.read_text())
|
||||
tpl = json.loads(os.path.expandvars(tpl_path.read_text()))
|
||||
|
||||
base: dict = {}
|
||||
if merge_into_existing and out_path.exists():
|
||||
base = json.loads(out_path.read_text())
|
||||
|
||||
merged: dict = {**base, **shared}
|
||||
merged[key] = {**base.get(key, {}), **shared.get(key, {}), **tpl.get(key, {})}
|
||||
|
||||
out_path.write_text(json.dumps(merged, indent=2) + "\n")
|
||||
|
||||
|
||||
def _deep_merge(base: dict, overlay: dict) -> dict:
|
||||
"""Recursively merge overlay into base; overlay wins on scalar/list keys."""
|
||||
out = dict(base)
|
||||
for k, v in overlay.items():
|
||||
if isinstance(out.get(k), dict) and isinstance(v, dict):
|
||||
out[k] = _deep_merge(out[k], v)
|
||||
else:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def _toml_value(value: object) -> str:
|
||||
"""Serialize a scalar/list as a TOML literal for a `codex -c` value.
|
||||
|
||||
bool is checked before int because `isinstance(True, int)` is True. Strings
|
||||
are emitted as JSON strings, which are valid TOML basic strings for the
|
||||
ASCII values our configs carry (commands, args, URLs). Tables never reach
|
||||
here -- they are flattened into dotted keys by _flatten.
|
||||
"""
|
||||
if isinstance(value, bool):
|
||||
return "true" if value else "false"
|
||||
if isinstance(value, (int, float)):
|
||||
return repr(value)
|
||||
if isinstance(value, str):
|
||||
return json.dumps(value)
|
||||
if isinstance(value, list):
|
||||
return "[" + ", ".join(_toml_value(v) for v in value) + "]"
|
||||
raise TypeError(f"unsupported TOML value type: {type(value).__name__}")
|
||||
|
||||
|
||||
_BARE_KEY = re.compile(r"^[A-Za-z0-9_-]+$")
|
||||
|
||||
|
||||
def _key_segment(seg: str) -> str:
|
||||
"""A dotted-key segment: bare if TOML-safe, else a quoted key."""
|
||||
return seg if _BARE_KEY.match(seg) else json.dumps(seg)
|
||||
|
||||
|
||||
def _flatten(obj: dict, prefix: list[str]):
|
||||
"""Yield (dotted-path-segments, leaf-value) for every non-table leaf.
|
||||
|
||||
Lists are leaves (TOML arrays), so we do not recurse into them; nested
|
||||
tables (e.g. an `env` table) are flattened into further dotted keys.
|
||||
"""
|
||||
for k, v in obj.items():
|
||||
path = prefix + [k]
|
||||
if isinstance(v, dict):
|
||||
yield from _flatten(v, path)
|
||||
else:
|
||||
yield path, v
|
||||
|
||||
|
||||
def emit_codex_args(shared_path: Path, tpl_path: Path) -> None:
|
||||
"""Print `dotted.key=<toml-value>` lines from the merged TOML chunks."""
|
||||
shared = tomllib.loads(os.path.expandvars(shared_path.read_text()))
|
||||
tpl = tomllib.loads(os.path.expandvars(tpl_path.read_text()))
|
||||
merged = _deep_merge(shared, tpl)
|
||||
for path, value in _flatten(merged, []):
|
||||
dotted = ".".join(_key_segment(s) for s in path)
|
||||
sys.stdout.write(f"{dotted}={_toml_value(value)}\n")
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__.split("\n\n", 1)[0],
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=("json", "codex-args"),
|
||||
required=True,
|
||||
help="Output mode: 'json' writes a merged file; 'codex-args' prints -c assignments.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--key",
|
||||
help="Top-level JSON key under which MCP entries live (required for --format json).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--merge-into-existing",
|
||||
action="store_true",
|
||||
help="json only: layer the merge on top of an existing <out> file, "
|
||||
"preserving entries already there (ours still win on name collision).",
|
||||
)
|
||||
parser.add_argument("shared", type=Path, help="Path to the shared chunk.")
|
||||
parser.add_argument("template", type=Path, help="Path to the port-placeholder template chunk.")
|
||||
parser.add_argument(
|
||||
"out",
|
||||
type=Path,
|
||||
nargs="?",
|
||||
help="Path the merged result is written to (json only; codex-args writes stdout).",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.format == "json":
|
||||
if not args.key:
|
||||
parser.error("--key is required when --format json")
|
||||
if args.out is None:
|
||||
parser.error("out is required when --format json")
|
||||
merge_json(args.shared, args.template, args.out, args.key, args.merge_into_existing)
|
||||
else: # codex-args
|
||||
if args.key:
|
||||
parser.error("--key is not accepted when --format codex-args")
|
||||
if args.merge_into_existing:
|
||||
parser.error("--merge-into-existing is not accepted when --format codex-args")
|
||||
if args.out is not None:
|
||||
parser.error("out path is not accepted when --format codex-args (result goes to stdout)")
|
||||
emit_codex_args(args.shared, args.template)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--cdp-endpoint=http://127.0.0.1:9222"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
# Workspace-independent MCP servers for the OpenAI Codex CLI.
|
||||
# This block is concatenated with the port-substituted templates/codex.toml
|
||||
# by manage.sh's write-instance-mcp-configs to produce .codex/config.toml at
|
||||
# the workspace root.
|
||||
|
||||
[mcp_servers.playwright]
|
||||
command = "npx"
|
||||
args = ["@playwright/mcp@latest", "--cdp-endpoint=http://127.0.0.1:9222"]
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"mcp": {
|
||||
"playwright": {
|
||||
"type": "local",
|
||||
"command": ["npx", "@playwright/mcp@latest", "--cdp-endpoint=http://127.0.0.1:9222"],
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
{
|
||||
"servers": {
|
||||
"playwright": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["@playwright/mcp@latest", "--cdp-endpoint=http://127.0.0.1:9222"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"penpot": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "http://localhost:${PENPOT_MCP_PORT}/mcp", "--allow-http"]
|
||||
},
|
||||
"serena-devenv": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-remote", "http://localhost:${SERENA_MCP_PORT}/mcp", "--allow-http"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
# Workspace-specific MCP servers for the OpenAI Codex CLI. The PENPOT_MCP_PORT
|
||||
# and SERENA_MCP_PORT placeholders below are filled in per workspace by
|
||||
# manage.sh's write-instance-mcp-configs, then the result is concatenated
|
||||
# with shared/codex.toml to produce .codex/config.toml.
|
||||
|
||||
[mcp_servers.penpot]
|
||||
url = "http://localhost:${PENPOT_MCP_PORT}/mcp"
|
||||
|
||||
[mcp_servers.serena-devenv]
|
||||
url = "http://localhost:${SERENA_MCP_PORT}/mcp"
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"mcp": {
|
||||
"penpot": {
|
||||
"type": "remote",
|
||||
"url": "http://localhost:${PENPOT_MCP_PORT}/mcp",
|
||||
"enabled": true
|
||||
},
|
||||
"serena-devenv": {
|
||||
"type": "remote",
|
||||
"url": "http://localhost:${SERENA_MCP_PORT}/mcp",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"servers": {
|
||||
"penpot": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:${PENPOT_MCP_PORT}/mcp"
|
||||
},
|
||||
"serena-devenv": {
|
||||
"type": "http",
|
||||
"url": "http://localhost:${SERENA_MCP_PORT}/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
# Penpot API configuration for error-reports CLI tool
|
||||
PENPOT_API_URI=http://localhost:3450
|
||||
PENPOT_ACCESS_TOKEN=your-access-token-here
|
||||
5
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
5
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -1,8 +1,7 @@
|
||||
description: Create a report to help us improve
|
||||
labels: ["bug"]
|
||||
name: Bug report
|
||||
type: Bug
|
||||
labels: ["needs triage"]
|
||||
|
||||
title: "bug: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1 +0,0 @@
|
||||
blank_issues_enabled: true
|
||||
5
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
5
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@ -1,8 +1,7 @@
|
||||
description: Suggest an idea for this project.
|
||||
labels: ["needs triage"]
|
||||
labels: ["needs triage", "enhancement"]
|
||||
name: "Feature request"
|
||||
type: Enhancement
|
||||
|
||||
title: "feature: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
38
.github/ISSUE_TEMPLATE/new-render-bug-report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/new-render-bug-report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
name: New Render Bug Report
|
||||
about: Create a report about the bugs you have found in the new render
|
||||
title: ''
|
||||
labels: new render
|
||||
assignees: claragvinola
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps to Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots or screen recordings**
|
||||
If applicable, add screenshots or screen recording to help illustrate your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -15,5 +15,6 @@
|
||||
- [ ] Add or modify existing integration tests in case of bugs or new features, if applicable.
|
||||
- [ ] Refactor any modified SCSS files following the refactor guide.
|
||||
- [ ] Check CI passes successfully.
|
||||
- [ ] Update the `CHANGES.md` file, referencing the related GitHub issue, if applicable.
|
||||
|
||||
<!-- For more details, check the contribution guidelines: https://github.com/penpot/penpot/blob/develop/CONTRIBUTING.md -->
|
||||
|
||||
76
.github/workflows/auto-label.yml
vendored
76
.github/workflows/auto-label.yml
vendored
@ -1,76 +0,0 @@
|
||||
name: Auto Label and Add to Project
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: triage-app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.TRIAGE_APP_ID }}
|
||||
private-key: ${{ secrets.TRIAGE_APP_PRIVATE_KEY }}
|
||||
owner: penpot
|
||||
|
||||
- name: Process Issue or PR
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ steps.triage-app-token.outputs.token }}
|
||||
script: |
|
||||
// === 1. CONFIGURATION ===
|
||||
const PROJECT_NUMBER = 8; // <--- Replace with your project board number
|
||||
const IS_ORG = true; // <--- Set to false if this is a personal project, true if an organization
|
||||
const OWNER = context.repo.owner;
|
||||
const REPO = context.repo.repo;
|
||||
|
||||
const issueNumber = context.issue.number;
|
||||
const isPR = !!context.payload.pull_request;
|
||||
const contentId = isPR ? context.payload.pull_request.node_id : context.payload.issue.node_id;
|
||||
|
||||
// Define your labels here
|
||||
const labelToApply = 'needs triage';
|
||||
|
||||
// === 2. APPLY THE LABEL ===
|
||||
console.log(`Applying label "${labelToApply}" to ${isPR ? 'PR' : 'Issue'} #${issueNumber}...`);
|
||||
await github.rest.issues.addLabels({
|
||||
issue_number: issueNumber,
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
labels: [labelToApply]
|
||||
});
|
||||
|
||||
// === 3. ADD TO PROJECT BOARD ===
|
||||
console.log(`Fetching Project #${PROJECT_NUMBER} ID...`);
|
||||
const projectQuery = `
|
||||
query($owner: String!, $number: Int!) {
|
||||
${IS_ORG ? 'organization' : 'user'}(login: $owner) {
|
||||
projectV2(number: $number) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const projectRes = await github.graphql(projectQuery, { owner: OWNER, number: PROJECT_NUMBER });
|
||||
const projectId = IS_ORG ? projectRes.organization.projectV2.id : projectRes.user.projectV2.id;
|
||||
|
||||
console.log(`Adding item to project board...`);
|
||||
const addToProjectMutation = `
|
||||
mutation($projectId: ID!, $contentId: ID!) {
|
||||
addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) {
|
||||
item {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await github.graphql(addToProjectMutation, { projectId, contentId });
|
||||
console.log("Automation successfully completed!");
|
||||
|
||||
108
.github/workflows/build-bundle.yml
vendored
108
.github/workflows/build-bundle.yml
vendored
@ -9,6 +9,16 @@ on:
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
build_wasm:
|
||||
description: 'BUILD_WASM. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
build_storybook:
|
||||
description: 'BUILD_STORYBOOK. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
workflow_call:
|
||||
inputs:
|
||||
gh_ref:
|
||||
@ -16,21 +26,25 @@ on:
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
|
||||
cancel-in-progress: true
|
||||
build_wasm:
|
||||
description: 'BUILD_WASM. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
build_storybook:
|
||||
description: 'BUILD_STORYBOOK. Valid values: yes, no'
|
||||
type: string
|
||||
required: false
|
||||
default: 'yes'
|
||||
|
||||
jobs:
|
||||
# ── 1. Decide whether there is anything to build ───────────────────────
|
||||
check:
|
||||
name: Check current bundle
|
||||
build-bundle:
|
||||
name: Build and Upload Penpot Bundle
|
||||
runs-on: penpot-runner-01
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
gh_ref: ${{ steps.vars.outputs.gh_ref }}
|
||||
bundle_version: ${{ steps.vars.outputs.bundle_version }}
|
||||
exists: ${{ steps.check.outputs.exists }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@ -45,52 +59,10 @@ jobs:
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
echo "bundle_version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
|
||||
|
||||
# The uploaded zip carries its version as S3 metadata. If the
|
||||
# existing object was already built from this same commit, the
|
||||
# whole build job is skipped.
|
||||
- name: Check if this bundle is already built
|
||||
id: check
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
EXISTING_VERSION=$(aws s3api head-object \
|
||||
--bucket ${{ secrets.S3_BUCKET }} \
|
||||
--key "penpot-${{ steps.vars.outputs.gh_ref }}.zip" \
|
||||
--query 'Metadata."bundle-version"' \
|
||||
--output text 2>/dev/null || echo "none")
|
||||
|
||||
if [ "$EXISTING_VERSION" = "${{ steps.vars.outputs.bundle_version }}" ]; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
{
|
||||
echo "### ⏭️ Bundle build skipped"
|
||||
echo ""
|
||||
echo "The bundle in S3 was already built from \`${{ steps.vars.outputs.bundle_version }}\`."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# ── 2. Build and upload, only when needed ──────────────────────────────
|
||||
build:
|
||||
name: Build and Upload Penpot Bundle
|
||||
runs-on: penpot-runner-01
|
||||
timeout-minutes: 90
|
||||
needs: check
|
||||
if: needs.check.outputs.exists == 'false'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
- name: Build bundle
|
||||
env:
|
||||
BUILD_WASM: 'yes'
|
||||
BUILD_STORYBOOK: 'yes'
|
||||
BUILD_WASM: ${{ inputs.build_wasm }}
|
||||
BUILD_STORYBOOK: ${{ inputs.build_storybook }}
|
||||
run: ./manage.sh build-bundle
|
||||
|
||||
- name: Prepare directories for zipping
|
||||
@ -104,32 +76,18 @@ jobs:
|
||||
zip -r zips/penpot.zip penpot
|
||||
|
||||
- name: Upload Penpot bundle to S3
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
aws s3 cp zips/penpot.zip \
|
||||
s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.check.outputs.gh_ref }}.zip \
|
||||
--metadata bundle-version=${{ needs.check.outputs.bundle_version }}
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip --metadata bundle-version=${{ steps.vars.outputs.bundle_version }}
|
||||
|
||||
# ── 3. Single failure notification for the whole workflow ─────────────
|
||||
notify:
|
||||
name: Notify failure
|
||||
runs-on: penpot-runner-01
|
||||
timeout-minutes: 5
|
||||
needs: [check, build]
|
||||
if: failure()
|
||||
|
||||
steps:
|
||||
- name: Notify Mattermost
|
||||
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
❌ 📦 *[PENPOT] Error building penpot bundles.*
|
||||
📄 Triggered from ref: `${{ needs.check.outputs.gh_ref || inputs.gh_ref }}`
|
||||
Bundle version: `${{ needs.check.outputs.bundle_version || 'n/a' }}`
|
||||
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
|
||||
Bundle version: `${{ steps.vars.outputs.bundle_version }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
|
||||
8
.github/workflows/build-develop.yml
vendored
8
.github/workflows/build-develop.yml
vendored
@ -11,6 +11,8 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "develop"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
@ -18,9 +20,3 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "develop"
|
||||
|
||||
build-admin-console-docker:
|
||||
uses: ./.github/workflows/build-docker-admin-console.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "develop"
|
||||
|
||||
91
.github/workflows/build-docker-admin-console.yml
vendored
91
.github/workflows/build-docker-admin-console.yml
vendored
@ -1,91 +0,0 @@
|
||||
name: Admin Console Docker Builder
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch or ref to build in penpot-nitrate'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
dispatch_ref:
|
||||
description: 'Branch of penpot-nitrate from which the workflow definition is read'
|
||||
type: string
|
||||
required: false
|
||||
default: 'develop'
|
||||
workflow_call:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch or ref to build in penpot-nitrate'
|
||||
type: string
|
||||
required: true
|
||||
dispatch_ref:
|
||||
description: 'Branch of penpot-nitrate from which the workflow definition is read'
|
||||
type: string
|
||||
required: false
|
||||
default: 'develop'
|
||||
secrets:
|
||||
ORG_WORKFLOW_TOKEN:
|
||||
description: 'Token with Actions write access on penpot-nitrate'
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-nitrate-docker:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.ORG_WORKFLOW_TOKEN }}
|
||||
REPO: penpot/penpot-nitrate
|
||||
WORKFLOW: build-docker-admin-console.yml
|
||||
GH_REF: ${{ inputs.gh_ref }}
|
||||
DISPATCH_REF: ${{ inputs.dispatch_ref }}
|
||||
steps:
|
||||
- name: Trigger nitrate docker build
|
||||
id: dispatch
|
||||
run: |
|
||||
DISTINCT_ID="${{ github.run_id }}-${{ github.run_attempt }}"
|
||||
CALLER_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
|
||||
gh workflow run "$WORKFLOW" --repo "$REPO" --ref "$DISPATCH_REF" \
|
||||
-f gh_ref="$GH_REF" \
|
||||
-f caller_run_id="$DISTINCT_ID" \
|
||||
-f caller_run_url="$CALLER_URL"
|
||||
|
||||
# Locate the dispatched run using the correlation id embedded in its run-name
|
||||
RUN_ID=""
|
||||
for i in $(seq 1 24); do
|
||||
sleep 5
|
||||
RUN_ID=$(gh run list --repo "$REPO" --workflow "$WORKFLOW" \
|
||||
--limit 10 --json databaseId,displayTitle \
|
||||
--jq ".[] | select(.displayTitle | contains(\"$DISTINCT_ID\")) | .databaseId" \
|
||||
| head -n1)
|
||||
[ -n "$RUN_ID" ] && break
|
||||
done
|
||||
|
||||
if [ -z "$RUN_ID" ]; then
|
||||
echo "::error::Could not locate the dispatched run in $REPO"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_URL="https://github.com/$REPO/actions/runs/$RUN_ID"
|
||||
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "run_url=$RUN_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice title=Nitrate docker build::$RUN_URL"
|
||||
|
||||
- name: Wait for nitrate docker build
|
||||
run: |
|
||||
gh run watch "${{ steps.dispatch.outputs.run_id }}" \
|
||||
--repo "$REPO" \
|
||||
--interval 30 \
|
||||
--exit-status
|
||||
|
||||
- name: Report result
|
||||
if: always() && steps.dispatch.outputs.run_id != ''
|
||||
run: |
|
||||
CONCLUSION=$(gh run view "${{ steps.dispatch.outputs.run_id }}" \
|
||||
--repo "$REPO" --json conclusion --jq '.conclusion')
|
||||
{
|
||||
echo "### 🐳 Nitrate docker build"
|
||||
echo ""
|
||||
echo "- Result: \`${CONCLUSION:-in_progress}\`"
|
||||
echo "- Run: ${{ steps.dispatch.outputs.run_url }}"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
22
.github/workflows/build-docker-devenv.yml
vendored
22
.github/workflows/build-docker-devenv.yml
vendored
@ -6,6 +6,7 @@ on:
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and push DevEnv Docker image
|
||||
environment: release-admins
|
||||
runs-on: penpot-runner-02
|
||||
|
||||
steps:
|
||||
@ -20,19 +21,12 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to Docker Registry (push destination)
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to Docker Hardened Images registry (base image pull)
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: dhi.io
|
||||
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||
|
||||
- name: Build and push DevEnv Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
env:
|
||||
@ -42,18 +36,6 @@ jobs:
|
||||
file: ./docker/devenv/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
tags: ${{ env.DOCKER_IMAGE }}:latest
|
||||
cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
|
||||
- name: Notify Mattermost
|
||||
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
🚀 *[PENPOT] New devenv available*
|
||||
📄 You may want to update your devenv.
|
||||
@alvaro
|
||||
|
||||
329
.github/workflows/build-docker.yml
vendored
329
.github/workflows/build-docker.yml
vendored
@ -16,121 +16,55 @@ on:
|
||||
required: true
|
||||
default: 'develop'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ inputs.gh_ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
ALL_IMAGES: backend frontend exporter storybook mcp
|
||||
# All runner instances live on the same server, so the bundle is
|
||||
# downloaded from S3 once and shared between build jobs through this
|
||||
# host-local directory. Each build job falls back to S3 if the file is
|
||||
# missing (e.g. if runners ever move to separate machines).
|
||||
BUNDLE_CACHE: /var/tmp/penpot-bundle-cache
|
||||
|
||||
jobs:
|
||||
# ── 1. Resolve the build key and check the whole set at once ───────────
|
||||
prepare:
|
||||
name: Prepare
|
||||
build-and-push:
|
||||
name: Build and Push Penpot Docker Images
|
||||
runs-on: penpot-runner-02
|
||||
timeout-minutes: 15
|
||||
outputs:
|
||||
gh_ref: ${{ steps.vars.outputs.gh_ref }}
|
||||
bundle_version: ${{ steps.vars.outputs.bundle_version }}
|
||||
build_key: ${{ steps.vars.outputs.build_key }}
|
||||
exists: ${{ steps.check.outputs.exists }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
- name: Extract some useful variables
|
||||
id: vars
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
GH_REF="${{ inputs.gh_ref || github.ref_name }}"
|
||||
echo "gh_ref=$GH_REF" >> $GITHUB_OUTPUT
|
||||
|
||||
BUNDLE_VERSION=$(aws s3api head-object \
|
||||
--bucket ${{ secrets.S3_BUCKET }} \
|
||||
--key "penpot-$GH_REF.zip" \
|
||||
--query 'Metadata."bundle-version"' \
|
||||
--output text)
|
||||
echo "bundle_version=$BUNDLE_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# Image content = bundle + docker build context, so the build key
|
||||
# combines both.
|
||||
CTX_HASH=$(git rev-parse "HEAD:docker/images" | cut -c1-12)
|
||||
echo "build_key=${BUNDLE_VERSION}-${CTX_HASH}" >> $GITHUB_OUTPUT
|
||||
|
||||
# The image set is a single block, so a single set-level check is
|
||||
# enough: `promote` drops a marker object in S3 only after every
|
||||
# image was built AND every branch tag was moved. Marker present
|
||||
# means there is nothing at all to do for this build key.
|
||||
- name: Check if this image set is already built
|
||||
id: check
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
if aws s3api head-object \
|
||||
--bucket ${{ secrets.S3_BUCKET }} \
|
||||
--key "markers/images-${{ steps.vars.outputs.build_key }}" \
|
||||
> /dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
{
|
||||
echo "### ⏭️ Image set build skipped"
|
||||
echo ""
|
||||
echo "The whole set was already built and promoted for \`${{ steps.vars.outputs.build_key }}\`."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
|
||||
# Stage the bundle in the host-local cache, once, for all the
|
||||
# build jobs. Download to a temp name and mv for atomicity;
|
||||
# prune stale bundles while at it.
|
||||
mkdir -p "$BUNDLE_CACHE"
|
||||
find "$BUNDLE_CACHE" -type f -mtime +1 -delete || true
|
||||
ZIP="$BUNDLE_CACHE/penpot-${{ steps.vars.outputs.build_key }}.zip"
|
||||
if [ ! -f "$ZIP" ]; then
|
||||
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
|
||||
mv "$ZIP.$$.tmp" "$ZIP"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 2. One build per image, in parallel, only when needed ──────────────
|
||||
build:
|
||||
name: Build ${{ matrix.image }}
|
||||
runs-on: penpot-runner-02
|
||||
timeout-minutes: 60
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.exists == 'false'
|
||||
strategy:
|
||||
fail-fast: true
|
||||
# 4 runner slots are available for build jobs on this server; cap the
|
||||
# matrix at 3 so short jobs (prepare and other workflows' checks)
|
||||
# never queue behind long builds.
|
||||
max-parallel: 3
|
||||
matrix:
|
||||
image: [backend, frontend, exporter, storybook, mcp]
|
||||
|
||||
steps:
|
||||
- name: Set common environment variables
|
||||
run: |
|
||||
# Each job execution will use its own docker configuration.
|
||||
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}-${{ matrix.image }}" >> $GITHUB_ENV
|
||||
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
- name: Extract some useful variables
|
||||
id: vars
|
||||
run: |
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download Penpot Bundles
|
||||
id: bundles
|
||||
env:
|
||||
FILE_NAME: penpot-${{ steps.vars.outputs.gh_ref }}.zip
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
tmp=$(aws s3api head-object \
|
||||
--bucket ${{ secrets.S3_BUCKET }} \
|
||||
--key "$FILE_NAME" \
|
||||
--query 'Metadata."bundle-version"' \
|
||||
--output text)
|
||||
echo "bundle_version=$tmp" >> $GITHUB_OUTPUT
|
||||
pushd docker/images
|
||||
aws s3 cp s3://${{ secrets.S3_BUCKET }}/$FILE_NAME .
|
||||
unzip $FILE_NAME > /dev/null
|
||||
mv penpot/backend bundle-backend
|
||||
mv penpot/frontend bundle-frontend
|
||||
mv penpot/exporter bundle-exporter
|
||||
mv penpot/storybook bundle-storybook
|
||||
mv penpot/mcp bundle-mcp
|
||||
popd
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
@ -147,140 +81,103 @@ jobs:
|
||||
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||
|
||||
# Images now build FROM Docker Hardened Images (dhi.io). DHI
|
||||
# is free (Apache 2.0, no subscription), but pulling from it
|
||||
# still requires an authenticated login -- a separate `docker
|
||||
# login` against a different registry host, even though it
|
||||
# reuses the same PUB_DOCKER_* credentials as the DockerHub
|
||||
# login above.
|
||||
- name: Login to Docker Hardened Images registry (base image pull)
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: dhi.io
|
||||
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||
|
||||
# Bundle staged once by `prepare` on this host; the S3 fallback only
|
||||
# triggers if the cache is unavailable (runners on another machine,
|
||||
# cache pruned mid-run, ...).
|
||||
- name: Prepare Penpot bundle
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
ZIP="$BUNDLE_CACHE/penpot-${{ needs.prepare.outputs.build_key }}.zip"
|
||||
if [ ! -f "$ZIP" ]; then
|
||||
echo "Bundle not found in host cache; falling back to S3."
|
||||
mkdir -p "$BUNDLE_CACHE"
|
||||
aws s3 cp "s3://${{ secrets.S3_BUCKET }}/penpot-${{ needs.prepare.outputs.gh_ref }}.zip" "$ZIP.$$.tmp"
|
||||
mv "$ZIP.$$.tmp" "$ZIP"
|
||||
fi
|
||||
# Extract only the bundle this job needs.
|
||||
pushd docker/images
|
||||
unzip -q "$ZIP" "penpot/${{ matrix.image }}/*"
|
||||
mv "penpot/${{ matrix.image }}" "bundle-${{ matrix.image }}"
|
||||
popd
|
||||
|
||||
- name: Set up QEMU (stable)
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Extract metadata (tags, labels)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ matrix.image }}
|
||||
images:
|
||||
frontend
|
||||
backend
|
||||
exporter
|
||||
storybook
|
||||
mcp
|
||||
labels: |
|
||||
bundle_version=${{ needs.prepare.outputs.bundle_version }}
|
||||
bundle_version=${{ steps.bundles.outputs.bundle_version }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
- name: Build and push Backend Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
env:
|
||||
DOCKER_IMAGE: 'backend'
|
||||
BUNDLE_PATH: './bundle-backend'
|
||||
with:
|
||||
context: ./docker/images/
|
||||
file: ./docker/images/Dockerfile.${{ matrix.image }}
|
||||
file: ./docker/images/Dockerfile.backend
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
# Immutable tag only; branch tags are moved atomically for the
|
||||
# whole image set by the `promote` job.
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:build-${{ needs.prepare.outputs.build_key }}
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ matrix.image }}:buildcache,mode=max
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
|
||||
# ── 3. Move the branch tags of ALL images together ─────────────────────
|
||||
# Runs only when every build succeeded (default `needs` semantics); if
|
||||
# the set was already complete, `build` is skipped and so is this job —
|
||||
# the S3 marker guarantees the branch tags were already moved.
|
||||
promote:
|
||||
name: Promote image set
|
||||
runs-on: penpot-runner-02
|
||||
timeout-minutes: 10
|
||||
needs: [prepare, build]
|
||||
|
||||
steps:
|
||||
- name: Set common environment variables
|
||||
run: |
|
||||
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to Docker Registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Point branch tags to the new build key
|
||||
run: |
|
||||
set -e
|
||||
for image in $ALL_IMAGES; do
|
||||
docker buildx imagetools create \
|
||||
-t "${{ secrets.DOCKER_REGISTRY }}/$image:${{ needs.prepare.outputs.gh_ref }}" \
|
||||
"${{ secrets.DOCKER_REGISTRY }}/$image:build-${{ needs.prepare.outputs.build_key }}"
|
||||
done
|
||||
|
||||
# The marker is written LAST: its presence certifies that all five
|
||||
# images exist and all branch tags point to this build key.
|
||||
- name: Write set-completed marker
|
||||
- name: Build and push Frontend Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
|
||||
run: |
|
||||
echo "${{ github.run_id }}" | aws s3 cp - \
|
||||
"s3://${{ secrets.S3_BUCKET }}/markers/images-${{ needs.prepare.outputs.build_key }}"
|
||||
{
|
||||
echo "### ✅ Image set promoted"
|
||||
echo ""
|
||||
echo "All \`:${{ needs.prepare.outputs.gh_ref }}\` tags now point to \`build-${{ needs.prepare.outputs.build_key }}\`."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
DOCKER_IMAGE: 'frontend'
|
||||
BUNDLE_PATH: './bundle-frontend'
|
||||
with:
|
||||
context: ./docker/images/
|
||||
file: ./docker/images/Dockerfile.frontend
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
|
||||
# ── 4. Single failure notification for the whole workflow ─────────────
|
||||
notify:
|
||||
name: Notify failure
|
||||
runs-on: penpot-runner-02
|
||||
timeout-minutes: 5
|
||||
needs: [prepare, build, promote]
|
||||
if: failure()
|
||||
- name: Build and push Exporter Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
env:
|
||||
DOCKER_IMAGE: 'exporter'
|
||||
BUNDLE_PATH: './bundle-exporter'
|
||||
with:
|
||||
context: ./docker/images/
|
||||
file: ./docker/images/Dockerfile.exporter
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
|
||||
- name: Build and push Storybook Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
env:
|
||||
DOCKER_IMAGE: 'storybook'
|
||||
BUNDLE_PATH: './bundle-storybook'
|
||||
with:
|
||||
context: ./docker/images/
|
||||
file: ./docker/images/Dockerfile.storybook
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
|
||||
- name: Build and push MCP Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
env:
|
||||
DOCKER_IMAGE: 'mcp'
|
||||
BUNDLE_PATH: './bundle-mcp'
|
||||
with:
|
||||
context: ./docker/images/
|
||||
file: ./docker/images/Dockerfile.mcp
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
|
||||
steps:
|
||||
- name: Notify Mattermost
|
||||
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
❌ 🐳 *[PENPOT] Error building/promoting the penpot docker image set.*
|
||||
📄 Triggered from ref: `${{ needs.prepare.outputs.gh_ref || inputs.gh_ref }}`
|
||||
📦 Bundle: `${{ needs.prepare.outputs.bundle_version || 'n/a' }}`
|
||||
❌ 🐳 *[PENPOT] Error building penpot docker images.*
|
||||
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
|
||||
📦 Bundle: `${{ steps.bundles.outputs.bundle_version }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
|
||||
22
.github/workflows/build-main-staging.yml
vendored
Normal file
22
.github/workflows/build-main-staging.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: _MAIN-STAGING
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '26 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-bundle:
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "main-staging"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
uses: ./.github/workflows/build-docker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "main-staging"
|
||||
8
.github/workflows/build-staging.yml
vendored
8
.github/workflows/build-staging.yml
vendored
@ -11,6 +11,8 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "staging"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
@ -18,9 +20,3 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "staging"
|
||||
|
||||
build-admin-console-docker:
|
||||
uses: ./.github/workflows/build-docker-admin-console.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "staging"
|
||||
|
||||
5
.github/workflows/build-tag.yml
vendored
5
.github/workflows/build-tag.yml
vendored
@ -12,6 +12,8 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: ${{ github.ref_name }}
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
@ -24,9 +26,10 @@ jobs:
|
||||
name: Notifications
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-docker
|
||||
|
||||
steps:
|
||||
- name: Notify Mattermost
|
||||
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
|
||||
2
.github/workflows/plugins-deploy-api-doc.yml
vendored
2
.github/workflows/plugins-deploy-api-doc.yml
vendored
@ -131,7 +131,7 @@ jobs:
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
|
||||
2
.github/workflows/plugins-deploy-package.yml
vendored
2
.github/workflows/plugins-deploy-package.yml
vendored
@ -114,7 +114,7 @@ jobs:
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
|
||||
@ -129,7 +129,7 @@ jobs:
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
|
||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -19,6 +19,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
environment: release-admins
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
version: ${{ steps.vars.outputs.gh_ref }}
|
||||
@ -103,7 +104,7 @@ jobs:
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@ae31bb6f9e26a54336e79696f108a2c91cf55b4e # v2.1.0
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
|
||||
85
.github/workflows/tests-backend.yml
vendored
85
.github/workflows/tests-backend.yml
vendored
@ -1,85 +0,0 @@
|
||||
name: "CI: Backend"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'common/**'
|
||||
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
paths:
|
||||
- 'backend/**'
|
||||
- 'common/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-backend:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Backend Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_USER: penpot_test
|
||||
POSTGRES_PASSWORD: penpot_test
|
||||
POSTGRES_DB: penpot_test
|
||||
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
redis:
|
||||
image: valkey/valkey:9
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Fmt
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
cljfmt check --parallel=true src/ test/
|
||||
|
||||
- name: Lint
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
clj-kondo --parallel --lint ../common/src/ src/
|
||||
|
||||
- name: Tests
|
||||
working-directory: ./backend
|
||||
env:
|
||||
PENPOT_TEST_DATABASE_URI: "postgresql://postgres/penpot_test"
|
||||
PENPOT_TEST_DATABASE_USERNAME: penpot_test
|
||||
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
|
||||
PENPOT_TEST_REDIS_URI: "redis://redis/1"
|
||||
|
||||
run: |
|
||||
mkdir -p /tmp/penpot;
|
||||
clojure -M:dev:test
|
||||
57
.github/workflows/tests-common.yml
vendored
57
.github/workflows/tests-common.yml
vendored
@ -1,57 +0,0 @@
|
||||
name: "CI: Common"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'common/**'
|
||||
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
paths:
|
||||
- 'common/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-common:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Common Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint
|
||||
working-directory: ./common
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt:clj
|
||||
pnpm run check-fmt:js
|
||||
pnpm run lint:clj
|
||||
|
||||
- name: Tests
|
||||
working-directory: ./common
|
||||
run: |
|
||||
./scripts/test
|
||||
69
.github/workflows/tests-composable-suite.yml
vendored
69
.github/workflows/tests-composable-suite.yml
vendored
@ -1,69 +0,0 @@
|
||||
name: "CI: Composable Test Suite"
|
||||
|
||||
# Runs the composable component test suite (it exercises component semantics
|
||||
# through the real Plugin API against the full frontend, so it needs the
|
||||
# frontend bundle + the plugin runtime, but no backend): the driver serves the
|
||||
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
|
||||
# fixtures. See plugins/apps/composable-test-suite/README.md ("Running in CI").
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
composable-test-suite:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Run composable test suite (mocked backend)"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# The driver serves the prebuilt bundle from frontend/resources/public.
|
||||
- name: Build frontend bundle
|
||||
working-directory: ./frontend
|
||||
run: ./scripts/build
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
working-directory: ./plugins
|
||||
run: pnpm --filter composable-test-suite exec playwright install --with-deps chromium
|
||||
|
||||
- name: Run composable test suite (mocked)
|
||||
working-directory: ./plugins
|
||||
run: pnpm --filter composable-test-suite run test:ci
|
||||
71
.github/workflows/tests-frontend.yml
vendored
71
.github/workflows/tests-frontend.yml
vendored
@ -1,71 +0,0 @@
|
||||
name: "CI: Frontend"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
- 'render-wasm/**'
|
||||
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
- 'render-wasm/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-frontend:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Frontend Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt:js
|
||||
pnpm run check-fmt:clj
|
||||
pnpm run check-fmt:scss
|
||||
pnpm run lint:clj
|
||||
pnpm run lint:js
|
||||
pnpm run lint:scss
|
||||
|
||||
- name: Unit Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test
|
||||
|
||||
- name: Component Tests
|
||||
working-directory: ./frontend
|
||||
env:
|
||||
VITEST_BROWSER_TIMEOUT: 120000
|
||||
run: |
|
||||
./scripts/test-components
|
||||
93
.github/workflows/tests-integration.yml
vendored
93
.github/workflows/tests-integration.yml
vendored
@ -1,93 +0,0 @@
|
||||
name: "CI: Integration"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
- 'render-wasm/**'
|
||||
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
- 'render-wasm/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-integration:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Build Integration Bundle"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build Bundle
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/build
|
||||
|
||||
- name: Store Bundle Cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
test-integration:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Integration Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
needs: build-integration
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test-e2e
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: integration-tests-result
|
||||
path: frontend/test-results/
|
||||
overwrite: true
|
||||
retention-days: 3
|
||||
58
.github/workflows/tests-library.yml
vendored
58
.github/workflows/tests-library.yml
vendored
@ -1,58 +0,0 @@
|
||||
name: "CI: Library"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'common/**'
|
||||
- 'library/**'
|
||||
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
paths:
|
||||
- 'common/**'
|
||||
- 'library/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-library:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Library Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint
|
||||
working-directory: ./library
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt
|
||||
pnpm run lint
|
||||
|
||||
- name: Tests
|
||||
working-directory: ./library
|
||||
run: |
|
||||
./scripts/test
|
||||
5
.github/workflows/tests-mcp.yml
vendored
5
.github/workflows/tests-mcp.yml
vendored
@ -45,8 +45,3 @@ jobs:
|
||||
pnpm run fmt:check;
|
||||
pnpm -r run build;
|
||||
pnpm -r run types:check;
|
||||
|
||||
- name: Tests
|
||||
working-directory: ./mcp
|
||||
run: |
|
||||
pnpm run test;
|
||||
|
||||
133
.github/workflows/tests-plugin-api-suite.yml
vendored
133
.github/workflows/tests-plugin-api-suite.yml
vendored
@ -1,133 +0,0 @@
|
||||
name: "CI: Plugin API Test Suite"
|
||||
|
||||
# Runs the Plugin API Test Suite (it exercises the real Penpot Plugin API, so it
|
||||
# needs a running frontend + the plugin runtime). Two jobs:
|
||||
#
|
||||
# - api-test-suite-mocked (pull_request / push): the per-PR gate. Serves the
|
||||
# prebuilt frontend bundle and intercepts every backend RPC with Playwright
|
||||
# (MOCK_BACKEND=1). No backend / no login. Validates the frontend Plugin API
|
||||
# binding + in-memory store; backend-result-dependent tests are skipped via the
|
||||
# `skipIfMocked` tag. See plugins/apps/plugin-api-test-suite/README.md.
|
||||
#
|
||||
# - api-test-suite-live (workflow_dispatch): true end-to-end against a LIVE
|
||||
# instance. Point PENPOT_BASE_URL at a reachable instance and provide login
|
||||
# credentials via repo secrets. Manual because the CI runner has no Docker to
|
||||
# stand up a full stack.
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
base_url:
|
||||
description: "Penpot base URL (e.g. https://localhost:3449)"
|
||||
required: false
|
||||
default: "https://localhost:3449"
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
- 'frontend/src/app/plugins/**'
|
||||
- 'common/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
api-test-suite-mocked:
|
||||
if: ${{ github.event_name != 'workflow_dispatch' && !github.event.pull_request.draft }}
|
||||
name: "Run Plugin API Test Suite (mocked)"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
# Mocked mode serves the prebuilt bundle from frontend/resources/public.
|
||||
- name: Build frontend bundle
|
||||
working-directory: ./frontend
|
||||
run: ./scripts/build
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
|
||||
- name: Install Playwright Chromium
|
||||
working-directory: ./plugins
|
||||
run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
|
||||
|
||||
- name: Generate API surface
|
||||
working-directory: ./plugins
|
||||
run: pnpm --filter plugin-api-test-suite run gen:api
|
||||
|
||||
- name: Run API test suite (mocked)
|
||||
working-directory: ./plugins
|
||||
env:
|
||||
MOCK_BACKEND: "1"
|
||||
run: pnpm --filter plugin-api-test-suite run test:ci
|
||||
|
||||
## The following job will launch the whole suite of tests but we need
|
||||
## to have a full environment in the CI for this to work.
|
||||
|
||||
# api-test-suite-live:
|
||||
# if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||
# name: Run Plugin API Test Suite (live)
|
||||
# runs-on: penpot-runner-02
|
||||
# container:
|
||||
# image: penpotapp/devenv:latest
|
||||
#
|
||||
# env:
|
||||
# PENPOT_BASE_URL: ${{ github.event.inputs.base_url }}
|
||||
# E2E_LOGIN_EMAIL: ${{ secrets.E2E_LOGIN_EMAIL }}
|
||||
# E2E_LOGIN_PASSWORD: ${{ secrets.E2E_LOGIN_PASSWORD }}
|
||||
#
|
||||
# steps:
|
||||
# - uses: actions/checkout@v6
|
||||
#
|
||||
# - name: Setup Node
|
||||
# uses: actions/setup-node@v6
|
||||
# with:
|
||||
# node-version-file: .nvmrc
|
||||
#
|
||||
# - name: Install deps
|
||||
# working-directory: ./plugins
|
||||
# run: |
|
||||
# corepack enable;
|
||||
# corepack install;
|
||||
# pnpm install;
|
||||
#
|
||||
# - name: Install Playwright Chromium
|
||||
# working-directory: ./plugins
|
||||
# run: pnpm --filter plugin-api-test-suite exec playwright install --with-deps chromium
|
||||
#
|
||||
# - name: Generate API surface
|
||||
# working-directory: ./plugins
|
||||
# run: pnpm --filter plugin-api-test-suite run gen:api
|
||||
#
|
||||
# # Note: requires a running Penpot instance reachable at PENPOT_BASE_URL.
|
||||
# - name: Run API test suite
|
||||
# working-directory: ./plugins
|
||||
# run: pnpm --filter plugin-api-test-suite run test:ci
|
||||
77
.github/workflows/tests-plugins.yml
vendored
77
.github/workflows/tests-plugins.yml
vendored
@ -1,77 +0,0 @@
|
||||
name: "CI: Plugins"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
paths:
|
||||
- 'plugins/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-plugins:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: Plugins Runtime Linter & Tests
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install -r;
|
||||
|
||||
- name: Run Lint
|
||||
working-directory: ./plugins
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Run Format Check
|
||||
working-directory: ./plugins
|
||||
run: pnpm run format:check
|
||||
|
||||
- name: Run Test
|
||||
working-directory: ./plugins
|
||||
run: pnpm run test
|
||||
|
||||
- name: Build runtime
|
||||
working-directory: ./plugins
|
||||
run: pnpm run build:runtime
|
||||
|
||||
- name: Build doc
|
||||
working-directory: ./plugins
|
||||
run: pnpm run build:doc
|
||||
|
||||
- name: Build plugins
|
||||
working-directory: ./plugins
|
||||
run: pnpm run build:plugins
|
||||
|
||||
- name: Build styles
|
||||
working-directory: ./plugins
|
||||
run: pnpm run build:styles-example
|
||||
57
.github/workflows/tests-wasm.yml
vendored
57
.github/workflows/tests-wasm.yml
vendored
@ -1,57 +0,0 @@
|
||||
name: "CI: WASM"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'render-wasm/**'
|
||||
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
paths:
|
||||
- 'render-wasm/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-render-wasm:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Render WASM Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Format
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
cargo fmt --check
|
||||
|
||||
- name: Lint
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
./lint
|
||||
|
||||
- name: Test
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
./test
|
||||
411
.github/workflows/tests.yml
vendored
Normal file
411
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,411 @@
|
||||
name: "CI"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Linter"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Lint Common
|
||||
working-directory: ./common
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt:clj
|
||||
pnpm run check-fmt:js
|
||||
pnpm run lint:clj
|
||||
|
||||
- name: Lint Frontend
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt:js
|
||||
pnpm run check-fmt:clj
|
||||
pnpm run check-fmt:scss
|
||||
pnpm run lint:clj
|
||||
pnpm run lint:js
|
||||
pnpm run lint:scss
|
||||
|
||||
- name: Lint Backend
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt
|
||||
pnpm run lint
|
||||
|
||||
- name: Lint Exporter
|
||||
working-directory: ./exporter
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt
|
||||
pnpm run lint
|
||||
|
||||
- name: Lint Library
|
||||
working-directory: ./library
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt
|
||||
pnpm run lint
|
||||
|
||||
test-common:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Common Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./common
|
||||
run: |
|
||||
./scripts/test
|
||||
|
||||
test-plugins:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: Plugins Runtime Linter & Tests
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
|
||||
- name: Run Lint
|
||||
working-directory: ./plugins
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Run Format Check
|
||||
working-directory: ./plugins
|
||||
run: pnpm run format:check
|
||||
|
||||
- name: Run Test
|
||||
working-directory: ./plugins
|
||||
run: pnpm run test
|
||||
|
||||
- name: Build runtime
|
||||
working-directory: ./plugins
|
||||
run: pnpm run build:runtime
|
||||
|
||||
- name: Build doc
|
||||
working-directory: ./plugins
|
||||
run: pnpm run build:doc
|
||||
|
||||
- name: Build plugins
|
||||
working-directory: ./plugins
|
||||
run: pnpm run build:plugins
|
||||
|
||||
- name: Build styles
|
||||
working-directory: ./plugins
|
||||
run: pnpm run build:styles-example
|
||||
|
||||
test-frontend:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Frontend Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Unit Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test
|
||||
|
||||
- name: Component Tests
|
||||
working-directory: ./frontend
|
||||
env:
|
||||
VITEST_BROWSER_TIMEOUT: 120000
|
||||
run: |
|
||||
./scripts/test-components
|
||||
|
||||
test-render-wasm:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Render WASM Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Format
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
cargo fmt --check
|
||||
|
||||
- name: Lint
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
./lint
|
||||
|
||||
- name: Test
|
||||
working-directory: ./render-wasm
|
||||
run: |
|
||||
./test
|
||||
|
||||
test-backend:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Backend Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17
|
||||
# Provide the password for postgres
|
||||
env:
|
||||
POSTGRES_USER: penpot_test
|
||||
POSTGRES_PASSWORD: penpot_test
|
||||
POSTGRES_DB: penpot_test
|
||||
|
||||
# Set health checks to wait until postgres has started
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
redis:
|
||||
image: valkey/valkey:9
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./backend
|
||||
env:
|
||||
PENPOT_TEST_DATABASE_URI: "postgresql://postgres/penpot_test"
|
||||
PENPOT_TEST_DATABASE_USERNAME: penpot_test
|
||||
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
|
||||
PENPOT_TEST_REDIS_URI: "redis://redis/1"
|
||||
|
||||
run: |
|
||||
mkdir -p /tmp/penpot;
|
||||
clojure -M:dev:test --reporter kaocha.report/documentation
|
||||
|
||||
test-library:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Library Tests"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./library
|
||||
run: |
|
||||
./scripts/test
|
||||
|
||||
build-integration:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Build Integration Bundle"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Build Bundle
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/build
|
||||
|
||||
- name: Store Bundle Cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
test-integration-1:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Integration Tests 1/3"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
needs: build-integration
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test-e2e --shard="1/3";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: integration-tests-result-1
|
||||
path: frontend/test-results/
|
||||
overwrite: true
|
||||
retention-days: 3
|
||||
|
||||
test-integration-2:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Integration Tests 2/3"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
needs: build-integration
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test-e2e --shard="2/3";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: integration-tests-result-2
|
||||
path: frontend/test-results/
|
||||
overwrite: true
|
||||
retention-days: 3
|
||||
|
||||
test-integration-3:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Integration Tests 3/3"
|
||||
runs-on: penpot-runner-02
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
needs: build-integration
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
key: "integration-bundle-${{ github.sha }}"
|
||||
path: frontend/resources/public
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/test-e2e --shard="3/3";
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: integration-tests-result-3
|
||||
path: frontend/test-results/
|
||||
overwrite: true
|
||||
retention-days: 3
|
||||
20
.gitignore
vendored
20
.gitignore
vendored
@ -9,21 +9,12 @@
|
||||
.clj-kondo
|
||||
.cpcache
|
||||
.lsp
|
||||
.env
|
||||
.nrepl-port
|
||||
.nyc_output
|
||||
.rebel_readline_history
|
||||
.repl
|
||||
opencode.json
|
||||
.opencode/package-lock.json
|
||||
/*.jpg
|
||||
/*.md
|
||||
!CHANGES.md
|
||||
!CONTRIBUTING.md
|
||||
!README.md
|
||||
!AGENTS.md
|
||||
!CODE_OF_CONDUCT.md
|
||||
!SECURITY.md
|
||||
/*.png
|
||||
/*.svg
|
||||
/*.sql
|
||||
@ -33,6 +24,7 @@ opencode.json
|
||||
/.clj-kondo/.cache
|
||||
/_dump
|
||||
/notes
|
||||
/.opencode/package-lock.json
|
||||
/plans
|
||||
/prompts
|
||||
/playground/
|
||||
@ -74,6 +66,8 @@ opencode.json
|
||||
/frontend/target/
|
||||
/frontend/test-results/
|
||||
/frontend/.shadow-cljs
|
||||
/other/
|
||||
/scripts/
|
||||
/nexus/
|
||||
/tmp/
|
||||
/vendor/**/target
|
||||
@ -93,14 +87,6 @@ opencode.json
|
||||
/.pnpm-store
|
||||
/.vscode
|
||||
/.idea
|
||||
*.iml
|
||||
/.claude
|
||||
/.playwright-mcp
|
||||
/.devenv/mcp/
|
||||
/opencode.json
|
||||
/.opencode/plans
|
||||
/.opencode/reports
|
||||
/.opencode/prompts
|
||||
/.ci-logs
|
||||
/.codex/
|
||||
/tools/__pycache__
|
||||
|
||||
@ -1,55 +1,28 @@
|
||||
---
|
||||
name: commiter
|
||||
description: Git commit assistant
|
||||
mode: subagent
|
||||
permission:
|
||||
read: allow
|
||||
glob: allow
|
||||
grep: allow
|
||||
edit: deny
|
||||
webfetch: deny
|
||||
websearch: deny
|
||||
task: deny
|
||||
skill: deny
|
||||
lsp: deny
|
||||
todowrite: deny
|
||||
question: deny
|
||||
external_directory: deny
|
||||
bash: allow
|
||||
mode: all
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are the Penpot commit assistant. You produce git commits that follow the
|
||||
repository's commit conventions. You do not implement features, review code, or
|
||||
push branches — you commit.
|
||||
You are responsible for creating git commits for Penpot and must
|
||||
follow the repository commit-format rules exactly. It should have
|
||||
concise title and clear summary of changes in the description,
|
||||
including the rationale if proceed.
|
||||
|
||||
## Required Reading
|
||||
## Requirements
|
||||
|
||||
Before drafting any commit, **read `.serena/memories/workflow/creating-commits.md`
|
||||
end-to-end**. It is the authoritative source for the commit message format, the
|
||||
emoji menu, subject/body limits, and the `AI-assisted-by` trailer. Follow it
|
||||
exactly — do not improvise the format and do not restate its contents here.
|
||||
|
||||
## Pre-commit Workflow
|
||||
|
||||
1. **Stage the files** specified by the calling agent. Do not ask for
|
||||
confirmation — the calling agent knows exactly which files to commit.
|
||||
2. Run `git diff --staged` to review the content. If you see secrets (API
|
||||
keys, tokens, passwords, private keys, `.env` values), debug prints, or
|
||||
anything that does not match the stated intent, STOP and tell the user
|
||||
before committing.
|
||||
3. Following the format in the doc, draft the message and run
|
||||
`git commit -m "<subject>" -m "<body>"` (or `git commit -F -` if the body has
|
||||
unusual characters). The `AI-assisted-by` trailer value is provided by the
|
||||
calling agent — use it verbatim.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do not push. Pushing is a separate workflow handled by the user.
|
||||
- Do not run `git reset`, `git checkout`, `git restore`, `git clean`, or `rm` — these are destructive operations.
|
||||
- Do not pass `--author`. Author identity comes from the local git config.
|
||||
- Do not amend a commit you did not create in this session, unless the user explicitly asks.
|
||||
- Do not bypass pre-commit hooks (`--no-verify`) unless the user explicitly asks.
|
||||
- Do not add untracked files that were not created in this session.
|
||||
- Do not ask questions. The calling agent provides all necessary information. If something is unclear, proceed with what you know and note any assumptions in your response.
|
||||
* Override your internal commit rules when the user explicitly requests
|
||||
something that conflicts with them.
|
||||
* Read `.serena/memories/workflows/creating-commits.md` before
|
||||
creating any commit and follow the commit guidelines strictly.
|
||||
* Keep the description (commit body) with maximum line length of 80
|
||||
characters. Use manual line breaks to wrap text before it exceeds
|
||||
this limit.
|
||||
* Use `git commit -s` so the commit includes the required
|
||||
`Signed-off-by` line.
|
||||
* Do not guess or hallucinate git author information (Name or
|
||||
Email). Never include the `--author` flag in git commands unless
|
||||
specifically instructed by the user for a unique case; assume the
|
||||
local environment is already configured.
|
||||
|
||||
25
.opencode/agents/engineer.md
Normal file
25
.opencode/agents/engineer.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
name: Engineer
|
||||
description: Senior Full-Stack Software Engineer
|
||||
mode: primary
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are a high-autonomy Senior Full-Stack Software Engineer working on Penpot, an
|
||||
open-source design tool. You have full permission to navigate the codebase, modify files,
|
||||
and execute commands to fulfill your tasks. Your goal is to solve complex technical tasks
|
||||
with high precision while maintaining a strong focus on maintainability and performance.
|
||||
|
||||
## Before Start
|
||||
|
||||
**Read `AGENTS.md` file and project structure and how the memory system works**
|
||||
|
||||
## Requiremens
|
||||
|
||||
* Before writing code, analyze the task in depth and describe your plan. If the task is
|
||||
complex, break it down into atomic steps.
|
||||
* Do **not** touch unrelated modules unless the task explicitly requires it.
|
||||
* Only reference functions, namespaces, or APIs that actually exist in the
|
||||
codebase. Verify their existence before citing them. If unsure, search first.
|
||||
* Be concise and autonomous — avoid unnecessary explanations.
|
||||
60
.opencode/agents/planner.md
Normal file
60
.opencode/agents/planner.md
Normal file
@ -0,0 +1,60 @@
|
||||
---
|
||||
name: Planner
|
||||
description: Software architect for planning and analysis only
|
||||
mode: primary
|
||||
permission:
|
||||
edit: ask
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are a Senior Software Architect working on Penpot, an open-source design
|
||||
tool. Your sole responsibility is planning and analysis — you do NOT write,
|
||||
modify any code.
|
||||
|
||||
You help users understand the codebase, design solutions, and create detailed
|
||||
implementation plans that other agents or developers can execute. Document
|
||||
everything they need to know: which files to touch for each task, code, testing,
|
||||
docs they might need to check, how to test it. Give them the whole plan as
|
||||
bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
|
||||
|
||||
Do **not** suggest commit messages or commit names anywhere in your plans or
|
||||
responses — committing is the developer's responsibility.
|
||||
|
||||
Assume they are a skilled developer, but know almost nothing about our toolset
|
||||
or problem domain. Assume they don't know good test design very well.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Analyze the codebase architecture and identify affected modules.
|
||||
* Read `AGENTS.md` file and project structure and how the memory system works and how to
|
||||
navigate and read relevant information conventions.
|
||||
* Break down complex features or bugs into atomic, actionable steps.
|
||||
* Propose solutions with clear rationale, trade-offs, and sequencing.
|
||||
* Identify risks, edge cases, and testing considerations.
|
||||
|
||||
Save plans to: plans/YYYY-MM-DD-<plan-one-line-title>.md
|
||||
|
||||
## Constraints
|
||||
|
||||
* You are **read-only** — never create, edit, or delete files.
|
||||
* You do **not** run builds, tests, linters, or any commands that modify state.
|
||||
* You do **not** create git commits or interact with version control.
|
||||
* You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
|
||||
`find`, `cat`).
|
||||
* Your output is a structured plan or analysis, ready for handoff to an
|
||||
engineer agent or developer.
|
||||
|
||||
## Output format
|
||||
|
||||
When producing a plan, structure it as:
|
||||
|
||||
1. **Context** — What is the problem or feature request?
|
||||
2. **Affected modules** — Which parts of the codebase are involved?
|
||||
3. **Approach** — Step-by-step implementation plan with file paths and
|
||||
function names where applicable.
|
||||
4. **Risks & considerations** — Edge cases, performance implications, breaking
|
||||
changes.
|
||||
5. **Testing strategy** — How to verify the implementation works correctly.
|
||||
|
||||
|
||||
56
.opencode/agents/prompt-assistant.md
Normal file
56
.opencode/agents/prompt-assistant.md
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
name: Prompt Assistant
|
||||
description: Refines and improves prompts for maximum clarity and effectiveness
|
||||
mode: all
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
You are an expert Prompt Engineer with strong knowledge of
|
||||
penpot. Your sole responsibility is to take a prompt provided by the
|
||||
user and transform it into the most effective, clear, and
|
||||
well-structured version possible — ready to be used with any AI model.
|
||||
|
||||
## Requirements
|
||||
|
||||
* You do NOT execute tasks. You do NOT write code. You only design and refine prompts
|
||||
* Read `AGENTS.md` file and project structure and how the memory system works and how to
|
||||
navigate and read relevant information conventions.
|
||||
* Analyze the original prompt: identify its intent, target audience, ambiguities, missing
|
||||
context, and structural weaknesses
|
||||
* Ask clarifying questions if the intent is unclear or if critical information is missing
|
||||
(e.g. target model, expected output format, tone, constraints). Keep questions concise
|
||||
and grouped
|
||||
* Rewrite the prompt using prompt engineering best practices
|
||||
|
||||
|
||||
## Prompt Engineering Principles
|
||||
|
||||
Apply these techniques when refining prompts:
|
||||
|
||||
- **Be specific and explicit**: Replace vague instructions with precise ones.
|
||||
- **Set the context**: Include background information the model needs to
|
||||
perform well.
|
||||
- **Specify the output format**: State the desired structure, length, tone,
|
||||
or format (e.g. bullet list, JSON, step-by-step).
|
||||
- **Add constraints**: Include what the model should avoid or not do.
|
||||
- **Use examples** (few-shot): When applicable, suggest adding examples to
|
||||
anchor the model's behaviour.
|
||||
- **Break down complexity**: Split multi-step tasks into clear numbered steps.
|
||||
- **Avoid ambiguity**: Remove pronouns and references that could be
|
||||
misinterpreted.
|
||||
- **Chain of thought**: For reasoning tasks, include "Think step by step."
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do NOT execute the prompt yourself.
|
||||
- Do NOT answer the question inside the prompt.
|
||||
- Do NOT add unnecessary verbosity — prompts should be as short as they can
|
||||
be while remaining complete.
|
||||
- Always preserve the user's original intent.
|
||||
|
||||
## Output
|
||||
|
||||
Refined Prompt: The improved, ready-to-use prompt. Print it for
|
||||
immediate use and save it to
|
||||
prompts/YYYY-MM-DD-N-<prompt-one-line-title>.md for future use.
|
||||
@ -1,43 +0,0 @@
|
||||
---
|
||||
description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the commiter subagent
|
||||
agent: build
|
||||
---
|
||||
|
||||
# Implement Plan
|
||||
|
||||
This command is run once a plan is ready (for example, from plan mode). Execute
|
||||
the plan already prepared in the current session context — it does not take
|
||||
extra arguments. Follow these steps in order.
|
||||
|
||||
## 1. Create the issue
|
||||
|
||||
Use the **`create-issue`** skill, following the *Creating Issues from Draft Body*
|
||||
flow in `mem:workflow/creating-issues`. Derive the issue title and body from the
|
||||
plan. Capture the new issue's number — call it **NNNN** (needed for the branch
|
||||
name and the commit reference).
|
||||
|
||||
## 2. Create the branch
|
||||
|
||||
Create and switch to a branch named after the issue:
|
||||
|
||||
```
|
||||
git checkout -b issue-NNNN
|
||||
```
|
||||
|
||||
(Replace NNNN with the issue number from step 1.)
|
||||
|
||||
## 3. Execute the plan
|
||||
|
||||
Implement the prepared plan from the session context. Work methodically, keeping
|
||||
changes focused on what the issue requires. Do not commit — the commit happens in
|
||||
step 4.
|
||||
|
||||
## 4. Commit with the commiter subagent
|
||||
|
||||
After the implementation is complete, delegate the commit to the **`commiter`**
|
||||
subagent. Give it a brief summary of what was implemented and why, the issue
|
||||
reference (`issue-NNNN`), and the model name you are running as so it sets the
|
||||
`AI-assisted-by` trailer correctly. The subagent owns the commit format and
|
||||
conventions.
|
||||
|
||||
Do not push. Pushing is handled separately by the user.
|
||||
@ -1,17 +0,0 @@
|
||||
Act as a senior software engineer and perform a thorough code review.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. Load the **`code-review-and-quality`** skill — it defines the five axes, core principles (DRY, KISS, YAGNI), severity taxonomy, and output format.
|
||||
2. Determine the diff or code to review from the provided context.
|
||||
3. Read the diff and the surrounding context for each changed file.
|
||||
4. Review across all five axes: correctness, readability, architecture, security, performance.
|
||||
5. Produce the review using the **Review Output** format from the skill (Summary → Critical/High → Other Findings → Refactoring → Testing Recommendations → Positive Observations → Final Verdict).
|
||||
6. For each finding: state the severity (Critical / High / Medium / Low / Suggestion), identify the file and line, describe failure circumstances, and propose a concrete fix.
|
||||
7. Do not invent problems. Every finding must be real and actionable.
|
||||
|
||||
Do not modify any code and do not create a commit — this command only reviews.
|
||||
|
||||
## Context
|
||||
|
||||
$ARGUMENTS
|
||||
@ -1,299 +0,0 @@
|
||||
import { tool } from "@opencode-ai/plugin"
|
||||
import path from "path"
|
||||
import { spawn } from "child_process"
|
||||
|
||||
const penpotPsqlTool = tool({
|
||||
description:
|
||||
"Execute a SQL command against the Penpot database. Uses the defaults from scripts/psql.",
|
||||
|
||||
args: {
|
||||
sql: tool.schema
|
||||
.string()
|
||||
.describe("SQL command to execute"),
|
||||
|
||||
test: tool.schema
|
||||
.boolean()
|
||||
.describe("Use the penpot_test database")
|
||||
.optional(),
|
||||
},
|
||||
|
||||
async execute(args, context) {
|
||||
const host = process.env.PENPOT_DB_HOST || "postgres"
|
||||
const user = process.env.PENPOT_DB_USER || "penpot"
|
||||
const db = args.test
|
||||
? "penpot_test"
|
||||
: process.env.PENPOT_DB_NAME || "penpot"
|
||||
const password = process.env.PENPOT_DB_PASSWORD || "penpot"
|
||||
|
||||
const psqlArgs = ["-h", host, "-U", user, "-d", db, "-c", args.sql]
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let stdout = ""
|
||||
let stderr = ""
|
||||
|
||||
const proc = spawn("psql", psqlArgs, {
|
||||
cwd: context.worktree,
|
||||
env: { ...process.env, PGPASSWORD: password },
|
||||
})
|
||||
|
||||
proc.stdout.on("data", (data) => {
|
||||
stdout += data.toString()
|
||||
})
|
||||
|
||||
proc.stderr.on("data", (data) => {
|
||||
stderr += data.toString()
|
||||
})
|
||||
|
||||
proc.on("error", (error) => {
|
||||
resolve(`Error: ${error.message}`)
|
||||
})
|
||||
|
||||
proc.on("close", (exitCode) => {
|
||||
const output =
|
||||
exitCode === 0
|
||||
? stdout.trim() || "Query executed successfully"
|
||||
: `Error (exit ${exitCode}): ${
|
||||
(stderr || stdout).trim() || "No error output"
|
||||
}`
|
||||
resolve(output)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const parenRepairTool = tool({
|
||||
description:
|
||||
"Fix mismatched parentheses/braces in Clojure files (.clj, .cljs, .cljc) then reformat with cljfmt.",
|
||||
|
||||
args: {
|
||||
// A string is used instead of an array so OpenCode displays it
|
||||
// in the generic tool invocation.
|
||||
files: tool.schema
|
||||
.string()
|
||||
.describe(
|
||||
"Comma-separated file paths to fix, for example: frontend/src/app/config.cljs, backend/src/core.clj",
|
||||
)
|
||||
.optional(),
|
||||
|
||||
code: tool.schema
|
||||
.string()
|
||||
.describe("Code string to fix via stdin")
|
||||
.optional(),
|
||||
},
|
||||
|
||||
async execute(args, context) {
|
||||
const script = path.join(context.worktree, "scripts/paren-repair")
|
||||
|
||||
const files = args.files
|
||||
? args.files
|
||||
.split(",")
|
||||
.map((file) => file.trim())
|
||||
.filter(Boolean)
|
||||
: []
|
||||
|
||||
const paramInfo =
|
||||
files.length > 0
|
||||
? `files=[${files.join(", ")}]`
|
||||
: args.code !== undefined
|
||||
? `code=(${args.code.length} chars)`
|
||||
: "none"
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const childArgs =
|
||||
files.length > 0
|
||||
? [script, ...files]
|
||||
: [script]
|
||||
|
||||
const proc = spawn("bb", childArgs, {
|
||||
cwd: context.worktree,
|
||||
})
|
||||
|
||||
let stdout = ""
|
||||
let stderr = ""
|
||||
|
||||
proc.stdout.on("data", (data) => {
|
||||
stdout += data.toString()
|
||||
})
|
||||
|
||||
proc.stderr.on("data", (data) => {
|
||||
stderr += data.toString()
|
||||
})
|
||||
|
||||
proc.on("error", (error) => {
|
||||
resolve(`Error: ${error.message}`)
|
||||
})
|
||||
|
||||
proc.on("close", (exitCode) => {
|
||||
const output =
|
||||
exitCode === 0
|
||||
? stdout.trim() || "No changes needed"
|
||||
: `Error (exit ${exitCode}): ${
|
||||
(stderr || stdout).trim() || "No error output"
|
||||
}`
|
||||
|
||||
resolve(output)
|
||||
})
|
||||
|
||||
// Close stdin in all cases so the process cannot wait indefinitely.
|
||||
if (args.code !== undefined) {
|
||||
proc.stdin.end(args.code)
|
||||
} else {
|
||||
proc.stdin.end()
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export default async function plugin() {
|
||||
return {
|
||||
tool: {
|
||||
"paren-repair": parenRepairTool,
|
||||
"penpot-psql": penpotPsqlTool,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// import { tool } from "@opencode-ai/plugin"
|
||||
// import path from "path"
|
||||
// import { spawn } from "child_process"
|
||||
|
||||
// function formatFiles(files) {
|
||||
// if (files.length === 0) return "stdin"
|
||||
|
||||
// // Keep the visible tool title reasonably short.
|
||||
// if (files.length <= 3) return files.join(", ")
|
||||
|
||||
// return `${files.slice(0, 3).join(", ")} (+${files.length - 3} more)`
|
||||
// }
|
||||
|
||||
// const parenRepairTool = tool({
|
||||
// description:
|
||||
// "Fix mismatched parentheses/braces in Clojure files, then reformat with cljfmt.",
|
||||
|
||||
// args: {
|
||||
// files: tool.schema
|
||||
// .array(tool.schema.string())
|
||||
// .describe("Array of file paths to fix")
|
||||
// .optional(),
|
||||
|
||||
// code: tool.schema
|
||||
// .string()
|
||||
// .describe("Code string to fix via stdin")
|
||||
// .optional(),
|
||||
// },
|
||||
|
||||
// async execute(args, context) {
|
||||
// const script = path.join(context.worktree, "scripts/paren-repair")
|
||||
|
||||
// const files = (args.files ?? []).map((file) => {
|
||||
// const absolute = path.isAbsolute(file)
|
||||
// ? file
|
||||
// : path.resolve(context.worktree, file)
|
||||
|
||||
// return path.relative(context.worktree, absolute)
|
||||
// })
|
||||
|
||||
// const targetSummary =
|
||||
// files.length > 0
|
||||
// ? formatFiles(files)
|
||||
// : args.code !== undefined
|
||||
// ? `stdin (${args.code.length} chars)`
|
||||
// : "no input"
|
||||
|
||||
// // This updates the tool-call title immediately, while it is running.
|
||||
// await context.metadata({
|
||||
// title: `Paren repair: ${targetSummary}`,
|
||||
// metadata: {
|
||||
// files,
|
||||
// codeChars: args.code?.length,
|
||||
// },
|
||||
// })
|
||||
|
||||
// const childArgs =
|
||||
// args.files && args.files.length > 0
|
||||
// ? [script, ...args.files]
|
||||
// : [script]
|
||||
|
||||
// return new Promise((resolve) => {
|
||||
// const proc = spawn("bb", childArgs, {
|
||||
// cwd: context.worktree,
|
||||
// })
|
||||
|
||||
// let stdout = ""
|
||||
// let stderr = ""
|
||||
|
||||
// if (args.code !== undefined) {
|
||||
// proc.stdin.end(args.code)
|
||||
// }
|
||||
|
||||
// proc.stdout.on("data", (data) => {
|
||||
// stdout += data.toString()
|
||||
// })
|
||||
|
||||
// proc.stderr.on("data", (data) => {
|
||||
// stderr += data.toString()
|
||||
// })
|
||||
|
||||
// proc.on("close", (exitCode) => {
|
||||
// const successful = exitCode === 0
|
||||
|
||||
// const commandOutput = successful
|
||||
// ? stdout.trim() || "No changes needed"
|
||||
// : `Error (exit ${exitCode}): ${(stderr || stdout).trim()}`
|
||||
|
||||
// const parameterOutput =
|
||||
// files.length > 0
|
||||
// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}`
|
||||
// : args.code !== undefined
|
||||
// ? `Input passed through stdin: ${args.code.length} characters`
|
||||
// : "No files or stdin input were passed"
|
||||
|
||||
// resolve({
|
||||
// title: `Paren repair: ${targetSummary}`,
|
||||
// output: `${parameterOutput}\n\n${commandOutput}`,
|
||||
// metadata: {
|
||||
// files,
|
||||
// codeChars: args.code?.length,
|
||||
// exitCode,
|
||||
// successful,
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
|
||||
// proc.on("error", (error) => {
|
||||
// resolve({
|
||||
// title: `Paren repair failed: ${targetSummary}`,
|
||||
// output: [
|
||||
// files.length > 0
|
||||
// ? `Files passed:\n${files.map((file) => `- ${file}`).join("\n")}`
|
||||
// : `Input: ${targetSummary}`,
|
||||
// `Failed to start bb: ${error.message}`,
|
||||
// ].join("\n\n"),
|
||||
// metadata: {
|
||||
// files,
|
||||
// codeChars: args.code?.length,
|
||||
// successful: false,
|
||||
// },
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// },
|
||||
// })
|
||||
|
||||
// export default async function plugin() {
|
||||
// return {
|
||||
// tool: {
|
||||
// "paren-repair": parenRepairTool,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
90
.opencode/skills/backport-commit/SKILL.md
Normal file
90
.opencode/skills/backport-commit/SKILL.md
Normal file
@ -0,0 +1,90 @@
|
||||
---
|
||||
name: backport-commit
|
||||
description: Port changes from a specific Git commit to the current branch by manually applying the diff, avoiding cherry-pick when it would introduce complex conflicts.
|
||||
---
|
||||
|
||||
# Backport Commit
|
||||
|
||||
Port changes from a specific Git commit to the current branch by manually
|
||||
applying the diff, avoiding `git cherry-pick` when it would introduce
|
||||
complex conflicts.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use this skill whenever the user asks to backport a commit, especially when:
|
||||
|
||||
- The commit touches multiple modules or files with significant divergence
|
||||
- `git cherry-pick` is explicitly ruled out ("do not use cherry-pick")
|
||||
- The target commit is old enough that conflicts are likely
|
||||
- The commit introduces both source changes AND new files (tests, etc.)
|
||||
- You need full control over how each hunk is applied
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Identify the target commit
|
||||
|
||||
```bash
|
||||
# Verify the commit exists and understand what it does
|
||||
git log --oneline -1 <commit-sha>
|
||||
|
||||
# Get the full diff (including new/deleted files)
|
||||
git show <commit-sha>
|
||||
|
||||
# Capture the original commit message for later reuse
|
||||
git log --format='%B' -1 <commit-sha>
|
||||
```
|
||||
|
||||
### 2. Identify affected modules
|
||||
|
||||
From the file paths in the diff, determine which Penpot modules are affected
|
||||
(frontend, backend, common, render-wasm, etc.) and read their `AGENTS.md`
|
||||
files **before** making any changes. If a module has no `AGENTS.md`, skip
|
||||
that step — verify with `ls <module>/AGENTS.md` first.
|
||||
|
||||
### 3. Read the current state of each affected file
|
||||
|
||||
For every file the diff touches, read the current version on disk to understand
|
||||
context and ensure correct placement before editing.
|
||||
|
||||
### 4. Apply changes manually (the core of this approach)
|
||||
|
||||
Process every hunk in the diff using the appropriate tool:
|
||||
|
||||
| Diff action | Tool to use |
|
||||
|-------------|-------------|
|
||||
| Modify existing file | `edit` — use enough surrounding context in `oldString` to uniquely match the location |
|
||||
| Add new file | `write` — include proper license header and namespace conventions matching project style |
|
||||
| Delete file | `bash rm <path>` |
|
||||
| Rename/move file | `bash mv <old> <new>`, then apply any content changes with `edit` |
|
||||
|
||||
> **Tip:** Group nearby hunks from the same file into a single `edit` call.
|
||||
> Use separate calls when hunks are far apart to keep `oldString` short and
|
||||
> unambiguous.
|
||||
|
||||
Repeat until **all** hunks in the diff are ported.
|
||||
|
||||
### 5. Validate
|
||||
|
||||
Run **lint**, **check-fmt**, and **tests** for every affected module (see each
|
||||
module's `AGENTS.md` for the exact commands). If the formatter auto-fixes
|
||||
indentation, verify the logic is still semantically correct. All checks must
|
||||
pass before moving on.
|
||||
|
||||
### 6. Port the changelog entry (if any)
|
||||
|
||||
If the original commit added or modified a `CHANGES.md` entry, port that entry
|
||||
too — adapting wording and version references for the target branch.
|
||||
|
||||
### 7. Commit
|
||||
|
||||
Ask the `commiter` sub-agent to create a commit. Stage all relevant files
|
||||
(exclude unrelated untracked files) and provide the original commit message as
|
||||
a reference, adapting it as needed for the target branch context.
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Context matters** — always read files before editing; never guess
|
||||
indentation or surrounding code
|
||||
- **Lint + format + test** — never skip validation before committing
|
||||
- **Preserve intent** — keep the original commit message meaning; the
|
||||
`commiter` agent handles formatting
|
||||
@ -1,255 +0,0 @@
|
||||
---
|
||||
name: code-review-and-quality
|
||||
description: Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch.
|
||||
---
|
||||
|
||||
# Code Review and Quality
|
||||
|
||||
## Overview
|
||||
|
||||
Multi-dimensional code review with quality gates. Every change gets reviewed before merge — no exceptions. Review covers five axes: correctness, readability, architecture, security, and performance.
|
||||
|
||||
**The approval standard:** Approve a change when it definitely improves overall code health, even if it isn't perfect. Perfect code doesn't exist — the goal is continuous improvement. Don't block a change because it isn't exactly how you would have written it. If it improves the codebase and follows the project's conventions, approve it.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Before merging any PR or change
|
||||
- After completing a feature implementation
|
||||
- When another agent or model produced code you need to evaluate
|
||||
- When refactoring existing code
|
||||
- After any bug fix (review both the fix and the regression test)
|
||||
|
||||
## Core Principles
|
||||
|
||||
These principles underpin every axis. When in doubt, default to them.
|
||||
|
||||
- **DRY (Don't Repeat Yourself):** Every piece of knowledge has one authoritative representation. If the same logic appears in two places, extract it into a shared helper, model, or type. Reviewers: flag duplicated logic as a required change — it's not "just similar," it's drift that will diverge.
|
||||
- **KISS (Keep It Simple, Stupid):** The simplest solution that works is the best solution. Complexity must earn its place. Reviewers: if you need more than one sentence to explain what a piece of code does, it's too complex — push for simplification before merge.
|
||||
- **YAGNI (You Aren't Gonna Need It):** Don't add abstractions, hooks, or generalizations for hypothetical future use cases. Generalize on the third occurrence, not the first. Reviewers: delete speculative generality.
|
||||
- **Don't invent problems:** Do not manufacture issues to produce more feedback. Every finding must be a real risk, a real readability barrier, or a real architectural concern — not a hypothetical or a stylistic preference disguised as a problem.
|
||||
|
||||
## The Five-Axis Review
|
||||
|
||||
Every review evaluates code across these dimensions.
|
||||
|
||||
### 1. Correctness
|
||||
|
||||
Does the code do what it claims to do?
|
||||
|
||||
- Does it match the spec or task requirements?
|
||||
- Are edge cases handled (null, empty, boundary values)?
|
||||
- Are error paths handled (not just the happy path)?
|
||||
- Does it pass all tests? Are the tests actually testing the right things?
|
||||
- Are there off-by-one errors, race conditions, or state inconsistencies?
|
||||
|
||||
### 2. Readability & Simplicity
|
||||
|
||||
Can another engineer (or agent) understand this code without the author explaining it?
|
||||
|
||||
- Are names descriptive and consistent with project conventions? (No `temp`, `data`, `result` without context)
|
||||
- Is the control flow straightforward (avoid nested ternaries, deep callbacks)?
|
||||
- Are there any "clever" tricks that should be simplified?
|
||||
- **KISS check:** Is this the simplest approach that solves the problem? A 20-line straightforward function beats a 5-line clever one that requires a comment to explain.
|
||||
- Could this be done in fewer lines? (1000 lines where 100 suffice is a failure)
|
||||
- Are abstractions earning their complexity? (Don't generalize until the third use case)
|
||||
- Is a new conditional bolted onto an unrelated flow? Push the logic into its own helper, state, or policy.
|
||||
- Do repeated conditionals on the same shape appear? They signal a missing model or dispatcher.
|
||||
- Are there dead code artifacts: no-op variables, backwards-compat shims, or `// removed` comments?
|
||||
|
||||
### 3. Architecture
|
||||
|
||||
Does the change fit the system's design?
|
||||
|
||||
- Does it follow existing patterns or introduce a new one? If new, is it justified?
|
||||
- Does it maintain clean module boundaries?
|
||||
- **DRY check:** Is there existing code that does the same thing? Reuse the canonical helper instead of writing a near-duplicate. If two branches do nearly the same thing, collapse them.
|
||||
- Are dependencies flowing in the right direction (no circular dependencies)?
|
||||
- Is the abstraction level appropriate (not over-engineered, not too coupled)?
|
||||
- Does this refactor reduce complexity or just relocate it? Count the concepts a reader must hold. Prefer the restructuring that makes whole branches disappear over one that re-centralizes the same logic. Prefer deleting an abstraction to polishing it.
|
||||
- Is feature-specific logic leaking into a shared or general-purpose module?
|
||||
- Are type boundaries explicit? Question gratuitous `any`/`unknown`/optional/casts and silent fallbacks.
|
||||
- **Structural remedies:** When you flag a problem, propose the move — not just the problem. Replace conditionals with dispatchers, collapse duplicate branches, separate orchestration from business logic, extract helpers, split large files. Prefer the remedy that removes moving pieces over one that spreads the same complexity around.
|
||||
|
||||
### 4. Security
|
||||
|
||||
For detailed security guidance, see `security-and-hardening`.
|
||||
|
||||
- Is user input validated and sanitized?
|
||||
- Are secrets kept out of code, logs, and version control?
|
||||
- Is authentication/authorization checked where needed?
|
||||
- Are SQL queries parameterized (no string concatenation)?
|
||||
- Are outputs encoded to prevent XSS?
|
||||
- Are dependencies from trusted sources with no known vulnerabilities?
|
||||
- Is data from external sources (APIs, logs, user content, config files) treated as untrusted?
|
||||
|
||||
### 5. Performance
|
||||
|
||||
- Any N+1 query patterns?
|
||||
- Any unbounded loops or unconstrained data fetching?
|
||||
- Any synchronous operations that should be async?
|
||||
- Any unnecessary re-renders in UI components?
|
||||
- Any missing pagination on list endpoints?
|
||||
- Any large objects created in hot paths?
|
||||
|
||||
## Review Process
|
||||
|
||||
1. **Understand the intent** — What is this change trying to accomplish? What spec or task does it implement?
|
||||
2. **Review tests first** — Tests reveal intent and coverage. Do they test behavior, not implementation details? Are edge cases covered?
|
||||
3. **Review the implementation** — Walk through each file with the five axes in mind.
|
||||
4. **Categorize findings** — Label every comment with its severity:
|
||||
|
||||
| Prefix | Meaning | Author Action |
|
||||
|--------|---------|---------------|
|
||||
| **Critical:** | Blocks merge | Security vulnerability, data loss, broken functionality |
|
||||
| **High:** | Required change | Must address before merge |
|
||||
| **Medium:** | Should fix | Strongly recommended, not a blocker |
|
||||
| **Low:** | Minor, optional | Author may ignore — formatting, style preferences |
|
||||
| **Suggestion:** | Worth considering | Not required, but improves the code |
|
||||
|
||||
For each finding, describe the circumstances under which it could fail: specific inputs, load conditions, timing, or user actions that trigger the problem. "This crashes when input is null" is actionable; "this might crash" is not.
|
||||
|
||||
Lead with what matters: correctness and security first, then structural issues, then everything else. A few high-conviction comments beat a long list.
|
||||
|
||||
5. **Verify the verification** — What tests were run? Did the build pass? Was the change tested manually? Screenshots for UI changes?
|
||||
|
||||
## Review Output
|
||||
|
||||
Structure every review using this format:
|
||||
|
||||
### Summary
|
||||
|
||||
Briefly explain what the code does and give an overall assessment.
|
||||
|
||||
### Critical and High-Priority Issues
|
||||
|
||||
List problems that could cause security incidents, data loss, crashes, incorrect behavior, or major performance degradation. For each: state the severity, identify the file/function/code section, explain why it's a problem, describe failure circumstances, and provide a concrete improvement with corrected code when useful.
|
||||
|
||||
### Other Findings
|
||||
|
||||
List medium- and low-priority issues, including maintainability and design concerns.
|
||||
|
||||
### Suggested Refactoring
|
||||
|
||||
Provide focused code changes or revised snippets. Preserve existing behavior unless a behavior change is explicitly justified.
|
||||
|
||||
### Testing Recommendations
|
||||
|
||||
Identify missing tests and describe specific test cases, including edge cases and failure scenarios.
|
||||
|
||||
### Positive Observations
|
||||
|
||||
Mention implementation choices that are clear, safe, efficient, or well designed. This is not fluff — it reinforces good patterns and tells the author what to keep doing.
|
||||
|
||||
### Final Verdict
|
||||
|
||||
Choose one:
|
||||
|
||||
- **Approve** — Ready to merge
|
||||
- **Approve with minor changes** — Good to merge after addressing low/medium issues
|
||||
- **Request changes** — Critical or high issues must be resolved before merge
|
||||
|
||||
## Change Sizing
|
||||
|
||||
Small, focused changes are easier to review, faster to merge, and safer to deploy.
|
||||
|
||||
```
|
||||
~100 lines changed → Good. Reviewable in one sitting.
|
||||
~300 lines changed → Acceptable if it's a single logical change.
|
||||
~1000 lines changed → Too large. Split it.
|
||||
```
|
||||
|
||||
**Watch file size, not just diff size.** Around 1000 *total* lines in a single file is a common inspection signal. When a change materially grows an already-large file, decompose first.
|
||||
|
||||
**Splitting strategies:**
|
||||
|
||||
| Strategy | How | When |
|
||||
|----------|-----|------|
|
||||
| **Stack** | Submit a small change, start the next one based on it | Sequential dependencies |
|
||||
| **By file group** | Separate changes for groups needing different reviewers | Cross-cutting concerns |
|
||||
| **Horizontal** | Create shared code/stubs first, then consumers | Layered architecture |
|
||||
| **Vertical** | Break into smaller full-stack slices of the feature | Feature work |
|
||||
|
||||
**Separate refactoring from feature work.** A change that refactors and adds new behavior is two changes — submit them separately.
|
||||
|
||||
## Change Descriptions
|
||||
|
||||
- **First line:** Short, imperative, standalone. "Delete the FizzBuzz RPC" not "Deleting the FizzBuzz RPC."
|
||||
- **Body:** What is changing and why. Include context and reasoning not visible in the code itself.
|
||||
- **Anti-patterns:** "Fix bug," "Fix build," "Add patch," "Phase 1."
|
||||
|
||||
## Dependencies
|
||||
|
||||
Before adding any dependency:
|
||||
|
||||
1. Does the existing stack solve this? (Often it does.)
|
||||
2. How large is the dependency? (Check bundle impact.)
|
||||
3. Is it actively maintained? (Check last commit, open issues.)
|
||||
4. Does it have known vulnerabilities? (`npm audit`)
|
||||
5. What's the license? (Must be compatible with the project.)
|
||||
|
||||
**Rule:** Prefer standard library and existing utilities over new dependencies. Every dependency is a liability.
|
||||
|
||||
**Upgrading dependencies:**
|
||||
|
||||
- Read the changelog, not just the version number. Semver is a promise the maintainer may not have kept.
|
||||
- One dependency per change. When a bulk bump breaks the build, you've lost which package did it.
|
||||
- Let the tests decide — a green suite before *and* after, not just "it installed."
|
||||
- Review the lockfile diff, not just `package.json`. Commit it and never hand-edit it.
|
||||
|
||||
For supply-chain risk triage, follow the `security-and-hardening` skill.
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "It works, that's good enough" | Working code that's unreadable, insecure, or architecturally wrong creates debt that compounds. |
|
||||
| "I wrote it, so I know it's correct" | Authors are blind to their own assumptions. Every change benefits from another set of eyes. |
|
||||
| "We'll clean it up later" | Later never comes. The review is the quality gate — use it. |
|
||||
| "AI-generated code is probably fine" | AI code needs more scrutiny, not less. It's confident and plausible, even when wrong. |
|
||||
| "The tests pass, so it's good" | Tests are necessary but not sufficient. They don't catch architecture, security, or readability problems. |
|
||||
| "The refactor makes it cleaner" | Relocating complexity isn't reducing it. If the reader still holds the same number of concepts, the structure didn't improve. |
|
||||
| "It's only a small addition to this file" | Small diffs still push files past healthy size and bolt branches onto unrelated flows. |
|
||||
| "It's just a version bump" | A bump is a behavior change you didn't write. Read the changelog. |
|
||||
| "I'll upgrade everything in one PR" | A bulk bump hides which package broke the build. One per change. |
|
||||
| "It's duplicated but it's only two places" | Two becomes three becomes five. Extract now, before the copies diverge. |
|
||||
| "The abstraction is future-proof" | YAGNI. Delete speculative generality — generalize on the third occurrence, not the first. |
|
||||
| "It's clever but efficient" | Cleverness is a readability tax. If it needs a comment to understand, simplify it. |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- PRs merged without any review
|
||||
- Review that only checks if tests pass (ignoring other axes)
|
||||
- "LGTM" without evidence of actual review
|
||||
- Security-sensitive changes without security-focused review
|
||||
- Large PRs that are "too big to review properly" (split them)
|
||||
- No regression tests with bug fix PRs
|
||||
- Accepting "I'll fix it later" — it never happens
|
||||
- A refactor that moves code around without reducing the number of concepts a reader must hold
|
||||
- New conditionals scattered into unrelated code paths (a missing abstraction)
|
||||
- A bespoke helper that duplicates an existing canonical one
|
||||
- A bulk "bump dependencies" PR with no changelog review
|
||||
|
||||
## Verification
|
||||
|
||||
After review is complete:
|
||||
|
||||
- [ ] All Critical issues are resolved
|
||||
- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification
|
||||
- [ ] Tests pass
|
||||
- [ ] Build succeeds
|
||||
- [ ] The verification story is documented (what changed, how it was verified)
|
||||
- [ ] Dependency upgrades reviewed against changelog, isolated per package, verified by green suite
|
||||
|
||||
## Multi-Model Review Pattern
|
||||
|
||||
Use different models for different review perspectives:
|
||||
|
||||
```
|
||||
Model A writes the code → Model B reviews → Model A addresses feedback → Human makes the final call
|
||||
```
|
||||
|
||||
Different models have different blind spots.
|
||||
|
||||
## See Also
|
||||
|
||||
- For detailed security review guidance, see `security-and-hardening`
|
||||
@ -1,27 +0,0 @@
|
||||
---
|
||||
name: create-issue
|
||||
description: Create or update GitHub issues (from PR, from draft body, retitle existing). Routes to the canonical flow in `mem:workflow/creating-issues`.
|
||||
---
|
||||
|
||||
# Skill: create-issue
|
||||
|
||||
Entry point for all GitHub issue work. All rules (title derivation, metadata,
|
||||
body templates, Issue Type IDs), all flows, and all `gh` / GraphQL commands
|
||||
live in `mem:workflow/creating-issues` (file:
|
||||
`.serena/memories/workflow/creating-issues.md`). This skill routes to the
|
||||
right flow.
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Create from PR** — PR exists; the issue is the changelog/release unit,
|
||||
the PR is the implementation. Issue = WHAT, PR = HOW.
|
||||
→ memory section **Creating Issues from PRs**
|
||||
- **Create from draft body** — Taiga story, user report, discussion; no PR
|
||||
yet.
|
||||
→ memory section **Creating Issues from Draft Body**
|
||||
- **Retitle existing issue** — current title is vague, prefixed, or stale.
|
||||
→ memory section **Retitling an Existing Issue**
|
||||
|
||||
Everything else (title derivation, metadata policy, body templates, Issue
|
||||
Type IDs, create/verify/cleanup commands) lives in the memory — go to the
|
||||
matching section there.
|
||||
@ -1,39 +0,0 @@
|
||||
---
|
||||
name: create-pr
|
||||
description: Create or update a GitHub PR following Penpot conventions.
|
||||
---
|
||||
|
||||
# Skill: create-pr
|
||||
|
||||
Create or update a GitHub PR. Read and follow:
|
||||
- `mem:workflow/creating-prs` — title format, description structure, writing principles
|
||||
- `mem:workflow/creating-commits` — commit type emojis
|
||||
|
||||
## When to Use
|
||||
|
||||
- Creating a new PR from a feature branch
|
||||
- Updating an existing PR's title or description to match conventions
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `gh` CLI authenticated (`gh auth status`)
|
||||
|
||||
## Commands
|
||||
|
||||
**Create:**
|
||||
|
||||
```bash
|
||||
gh pr create --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
|
||||
```
|
||||
|
||||
**Update:**
|
||||
|
||||
```bash
|
||||
gh pr edit <NUMBER> --repo penpot/penpot --title "<TITLE>" --body-file /tmp/pr-body.md
|
||||
```
|
||||
|
||||
**Verify:**
|
||||
|
||||
```bash
|
||||
gh pr view <NUMBER> --repo penpot/penpot --json title,body
|
||||
```
|
||||
230
.opencode/skills/gh-issue-from-pr/SKILL.md
Normal file
230
.opencode/skills/gh-issue-from-pr/SKILL.md
Normal file
@ -0,0 +1,230 @@
|
||||
---
|
||||
name: gh-issue-from-pr
|
||||
description: Create a user-facing GitHub issue from a PR, separating the WHAT from the HOW, with correct milestone, project, labels, and issue type.
|
||||
---
|
||||
|
||||
# Skill: gh-issue-from-pr
|
||||
|
||||
Create a GitHub issue that captures the **WHAT** (user-facing feature or
|
||||
bug) from an existing PR that describes the **HOW** (implementation).
|
||||
Used when the project board needs an issue as the primary changelog/release unit.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Create a tracking issue from a PR for changelog purposes
|
||||
- Extract the user-facing problem/feature from a PR's implementation details
|
||||
- Assign milestone, project, labels, and issue type to a new issue derived from a PR
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `gh` CLI authenticated (`gh auth status`)
|
||||
- Permission to create issues and edit PRs in the target repository
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Understand the PR
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot \
|
||||
--json title,body,author,labels,baseRefName,mergedAt,state,milestone
|
||||
```
|
||||
|
||||
Identify:
|
||||
|
||||
- **WHAT** — user-facing problem or feature. Goes into the issue.
|
||||
Describe symptoms and impact, not internal mechanisms.
|
||||
- **HOW** — implementation details. These belong in the PR, not the issue.
|
||||
|
||||
### 2. Determine metadata
|
||||
|
||||
| Field | Source | Rule |
|
||||
|-------|--------|------|
|
||||
| **Title** | PR title | Rewrite from user perspective. Strip leading emoji prefixes (`:bug:`, `:sparkles:`, `:tada:`). Focus on observable behavior. Use imperative mood. |
|
||||
| **Labels** | PR labels | Copy user-facing labels (`bug`, `enhancement`, `community contribution`). Skip workflow labels (`backport candidate`, `team-qa`). |
|
||||
| **Milestone** | PR milestone | **Always copy what's on the PR.** Fetch with: `gh pr view <PR_NUMBER> --json milestone --jq '.milestone.title'` If the PR has no milestone, create the issue without one. |
|
||||
| **Project** | Always `Main` | Penpot uses the `Main` project (number 8) for all issues. |
|
||||
| **Body** | PR's user-facing section | Extract steps to reproduce or feature description. Omit internal details. Use templates below. |
|
||||
| **Issue Type** | PR labels / title | Map: `bug` label or `:bug:` title → `Bug`. `enhancement` label or `:sparkles:` title → `Enhancement`. Feature/epic → `Feature`. Default → `Task`. |
|
||||
|
||||
### 3. Write the issue body
|
||||
|
||||
**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>
|
||||
```
|
||||
|
||||
### 4. Create the issue
|
||||
|
||||
Write the body to a temp file to avoid shell quoting issues:
|
||||
|
||||
```bash
|
||||
cat > /tmp/issue-body.md << 'ISSUE_BODY'
|
||||
<body content here>
|
||||
ISSUE_BODY
|
||||
```
|
||||
|
||||
Create:
|
||||
|
||||
```bash
|
||||
gh issue create \
|
||||
--repo penpot/penpot \
|
||||
--title "<Title>" \
|
||||
--label "<label1>" \
|
||||
--label "<label2>" \
|
||||
--milestone "<milestone>" \
|
||||
--project "Main" \
|
||||
--body-file /tmp/issue-body.md
|
||||
```
|
||||
|
||||
Output: `https://github.com/penpot/penpot/issues/<NUMBER>`
|
||||
|
||||
### 5. Assign to the PR author
|
||||
|
||||
Assign the issue to the PR author so they're responsible for it:
|
||||
|
||||
```bash
|
||||
AUTHOR=$(gh pr view <PR_NUMBER> --repo penpot/penpot --json author --jq '.author.login')
|
||||
gh issue edit <ISSUE_NUMBER> --repo penpot/penpot --add-assignee "$AUTHOR"
|
||||
```
|
||||
|
||||
### 6. Set the Issue Type
|
||||
|
||||
`gh issue create` can't set the Issue Type directly. Use GraphQL.
|
||||
|
||||
Get the issue's GraphQL node ID:
|
||||
|
||||
```bash
|
||||
ISSUE_ID=$(gh api graphql -f query='
|
||||
query { repository(owner: "penpot", name: "penpot") {
|
||||
issue(number: <ISSUE_NUMBER>) { id }
|
||||
}}' --jq '.data.repository.issue.id')
|
||||
```
|
||||
|
||||
Issue Type IDs for the Penpot repo:
|
||||
|
||||
| Type | ID |
|
||||
|------|----|
|
||||
| Bug | `IT_kwDOAcyBPM4AX5Nb` |
|
||||
| Enhancement | `IT_kwDOAcyBPM4B_IQN` |
|
||||
| Feature | `IT_kwDOAcyBPM4AX5Nf` |
|
||||
| Task | `IT_kwDOAcyBPM4AX5NY` |
|
||||
| Question | `IT_kwDOAcyBPM4B_IQj` |
|
||||
| Docs | `IT_kwDOAcyBPM4B_IQz` |
|
||||
|
||||
Set it:
|
||||
|
||||
```bash
|
||||
gh api graphql -f query='
|
||||
mutation {
|
||||
updateIssue(input: {
|
||||
id: "'"$ISSUE_ID"'"
|
||||
issueTypeId: "<TYPE_ID>"
|
||||
}) {
|
||||
issue { number issueType { name } }
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
### 7. Verify
|
||||
|
||||
```bash
|
||||
gh issue view <ISSUE_NUMBER> --repo penpot/penpot \
|
||||
--json title,milestone,projectItems,labels \
|
||||
--jq '{title, milestone: .milestone.title, projects: [.projectItems[].title], labels: [.labels[].name]}'
|
||||
|
||||
gh api graphql -f query='
|
||||
query { repository(owner: "penpot", name: "penpot") {
|
||||
issue(number: <ISSUE_NUMBER>) { issueType { name } }
|
||||
}}' --jq '.data.repository.issue.issueType.name'
|
||||
```
|
||||
|
||||
### 8. Link the PR to the issue
|
||||
|
||||
Append `Closes #<ISSUE_NUMBER>` to the PR body:
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot --json body --jq '.body' > /tmp/pr-body.md
|
||||
printf "\n\nCloses #<ISSUE_NUMBER>\n" >> /tmp/pr-body.md
|
||||
gh pr edit <PR_NUMBER> --repo penpot/penpot --body-file /tmp/pr-body.md
|
||||
|
||||
# Verify
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot --json body \
|
||||
--jq '.body | test("Closes #<ISSUE_NUMBER>")'
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
### 9. Clean up
|
||||
|
||||
```bash
|
||||
rm -f /tmp/issue-body.md /tmp/pr-body.md
|
||||
```
|
||||
|
||||
## Label rules
|
||||
|
||||
| PR has | Issue gets |
|
||||
|--------|-----------|
|
||||
| `bug` | `bug` |
|
||||
| `enhancement` | `enhancement` |
|
||||
| `community contribution` | `community contribution` |
|
||||
| `backport candidate` | *(skip — workflow label)* |
|
||||
| `team-qa` | *(skip — workflow label)* |
|
||||
| No user-facing label | Infer from title: `:bug:` → `bug`, `:sparkles:` → `enhancement` |
|
||||
|
||||
## Issue Type mapping
|
||||
|
||||
| PR label(s) / title prefix | Issue Type |
|
||||
|----------------------------|-----------|
|
||||
| `bug` or `:bug:` | Bug |
|
||||
| `enhancement` or `:sparkles:` or `:tada:` | Enhancement |
|
||||
| Feature / epic | Feature |
|
||||
| Documentation | Docs |
|
||||
| None of the above | Task |
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Issue = WHAT, PR = HOW.** Never put implementation details in the
|
||||
issue body. The issue is for users, QA, and changelog readers.
|
||||
- **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** — `Closes #<NUMBER>` 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.
|
||||
- **Community attribution:** if the PR has the `community contribution`
|
||||
label or the author is not a core team member, add the label to the issue.
|
||||
@ -1,25 +1,31 @@
|
||||
---
|
||||
name: nrepl-eval
|
||||
description: Evaluate Clojure code via nREPL using the standalone scripts/nrepl-eval.mjs CLI tool.
|
||||
description: Evaluate Clojure code via nREPL using the standalone tools/nrepl-eval.mjs CLI tool.
|
||||
---
|
||||
|
||||
# nREPL Eval
|
||||
|
||||
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
|
||||
`scripts/nrepl-eval.mjs`.
|
||||
`tools/nrepl-eval.mjs` — a standalone CLI application.
|
||||
|
||||
Full documentation: `mem:scripts/nrepl-eval` (file: `.serena/memories/scripts/nrepl-eval.md`)
|
||||
Session state (defs, in-ns, etc.) persists across invocations via a stored
|
||||
session ID, so you can build up state incrementally.
|
||||
|
||||
## Quick Reference
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs [options] [<code>]
|
||||
node tools/nrepl-eval.mjs [options] [<code>]
|
||||
```
|
||||
|
||||
The tool is also executable directly:
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs [options] [<code>]
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--backend` | Connect to backend nREPL (port 6064) | — |
|
||||
| `--frontend` | Connect to frontend nREPL (port 3447) | — |
|
||||
| `-p, --port PORT` | nREPL server port | `6064` |
|
||||
| `-H, --host HOST` | nREPL server host | `127.0.0.1` |
|
||||
| `-t, --timeout MS` | Timeout in milliseconds | `120000` |
|
||||
@ -27,11 +33,88 @@ Full documentation: `mem:scripts/nrepl-eval` (file: `.serena/memories/scripts/nr
|
||||
| `-e, --last-error` | Evaluate `*e` to retrieve the last exception | — |
|
||||
| `-h, --help` | Show help message | — |
|
||||
|
||||
## Examples
|
||||
## When to Use
|
||||
|
||||
Use this tool when you need to:
|
||||
|
||||
1. **Evaluate Clojure code** during development — test functions, inspect
|
||||
state, or run experiments against a running Clojure process.
|
||||
2. **Verify that edited files compile** — require namespaces with `:reload`
|
||||
to pick up changes.
|
||||
3. **Inspect the last exception** after a failed evaluation — use `-e` to
|
||||
print the error stored in `*e`.
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Session management
|
||||
|
||||
Sessions are persisted to `/tmp/penpot-nrepl-session-<host>-<port>`. State
|
||||
carries across calls automatically:
|
||||
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs '(+ 1 2 3)'
|
||||
./scripts/nrepl-eval.mjs --backend '(+ 1 2 3)'
|
||||
./scripts/nrepl-eval.mjs --frontend '(js/alert "hi")'
|
||||
./scripts/nrepl-eval.mjs -e
|
||||
./tools/nrepl-eval.mjs '(def x 42)'
|
||||
./tools/nrepl-eval.mjs 'x'
|
||||
# => 42
|
||||
```
|
||||
|
||||
Reset the session to start fresh:
|
||||
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs --reset-session '(def x 0)'
|
||||
```
|
||||
|
||||
### 2. Evaluate code
|
||||
|
||||
**Single expression (inline) — uses default port 6064:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs '(+ 1 2 3)'
|
||||
```
|
||||
|
||||
**Multiple expressions via heredoc (recommended — avoids escaping issues):**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs <<'EOF'
|
||||
(def x 10)
|
||||
(+ x 20)
|
||||
EOF
|
||||
```
|
||||
|
||||
**Override with a different port:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs -p 7888 '(+ 1 2 3)'
|
||||
```
|
||||
|
||||
### 3. Inspect last exception
|
||||
|
||||
After code throws an error, retrieve the full exception details:
|
||||
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs -e
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Require a namespace with reload:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)"
|
||||
```
|
||||
|
||||
**Test a function:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs "(ns/my-function arg1 arg2)"
|
||||
```
|
||||
|
||||
**Long-running operation with custom timeout:**
|
||||
```bash
|
||||
./tools/nrepl-eval.mjs -t 300000 "(long-running-fn)"
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Default port is 6064** — just pass code directly, no `-p` needed when
|
||||
your nREPL server is on 6064. Use `-p <PORT>` for a different port.
|
||||
- **Always use `:reload`** when requiring namespaces to pick up file changes.
|
||||
- **Session is reused** across invocations — defs, in-ns, and var bindings
|
||||
persist. Use `--reset-session` to clear.
|
||||
- **Do not start any server** — the tool connects to an existing nREPL
|
||||
server, it is not the agent's responsibility to start the nREPL server
|
||||
(assume the server is already running on the specified port).
|
||||
|
||||
@ -1,271 +0,0 @@
|
||||
---
|
||||
name: planner
|
||||
description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to .opencode/plans/YYYY-MM-DD-<title>.md.
|
||||
---
|
||||
|
||||
# Planner
|
||||
|
||||
Read-only senior software architect role for Penpot. Produces structured
|
||||
implementation plans that engineers or other agents can execute. Never writes
|
||||
or modifies code.
|
||||
|
||||
## When to Use
|
||||
|
||||
- The user asks for a plan, design, or analysis of a feature or bug.
|
||||
- The user wants to understand which parts of the codebase a task will touch.
|
||||
- The user needs a step-by-step implementation plan with file paths, function
|
||||
names, and test strategy.
|
||||
- The user asks "how would I implement X?" or "what's involved in fixing Y?".
|
||||
- The user is about to start non-trivial work and wants a bite-sized task
|
||||
breakdown.
|
||||
|
||||
Do **not** use this skill to actually implement anything — it is read-only.
|
||||
|
||||
## Role
|
||||
|
||||
You are a Senior Software Architect working on Penpot, an open-source design
|
||||
tool. Your sole responsibility is planning and analysis — you do NOT write or
|
||||
modify code.
|
||||
|
||||
You help users understand the codebase, design solutions, and create detailed
|
||||
implementation plans that other agents or developers can execute. Document
|
||||
everything they need to know: which files to touch for each task, code patterns,
|
||||
tests, and how to verify correctness. Apply DRY and KISS principles.
|
||||
|
||||
Do **not** suggest commit messages or commit names anywhere in your plans or
|
||||
responses — committing is the developer's responsibility.
|
||||
|
||||
## Required Reading Before Planning
|
||||
|
||||
Before drafting any plan, work through the project's own guidance:
|
||||
|
||||
1. Read `critical-info` (`.serena/memories/critical-info.md`) — the entry point
|
||||
that describes the monorepo structure and module dependency graph.
|
||||
2. From `critical-info`, identify which modules your task affects.
|
||||
3. Read each affected module's core memory, e.g. `mem:frontend/core`,
|
||||
`mem:backend/core`, `mem:common/core`, `mem:exporter/core`,
|
||||
`mem:render-wasm/core`. Follow `mem:` references deeper as needed.
|
||||
4. For each affected module, note its lint, format, and test commands so the
|
||||
plan can include concrete verification steps.
|
||||
|
||||
Skipping this step is the #1 cause of incorrect or incomplete plans.
|
||||
|
||||
## The Planning Process
|
||||
|
||||
### Phase 1: Architecture Analysis
|
||||
|
||||
1. Read the spec, requirements, or feature request.
|
||||
2. Analyze the codebase architecture and identify affected modules.
|
||||
3. Read project conventions (starting with `critical-info` and module core
|
||||
memories) before drafting.
|
||||
4. Map dependencies between components (see the dependency graph in
|
||||
`critical-info`).
|
||||
5. Identify risks, edge cases, performance implications, and breaking changes.
|
||||
|
||||
### Phase 2: Task Breakdown
|
||||
|
||||
Implementation order follows the monorepo's dependency graph:
|
||||
`frontend -> common`, `backend -> common`, `exporter -> common`,
|
||||
`frontend -> render-wasm`. Build shared foundations first, then layer
|
||||
consumers on top.
|
||||
|
||||
#### Slice Vertically
|
||||
|
||||
Instead of building all of common, then all of backend, then all of frontend —
|
||||
build one complete feature path at a time:
|
||||
|
||||
```
|
||||
Task 1: common data types + schema ← foundation
|
||||
Task 2: backend RPC handler + persistence
|
||||
Task 3: frontend UI component + API integration
|
||||
```
|
||||
|
||||
Each vertical slice delivers working, testable functionality.
|
||||
|
||||
#### Write Tasks
|
||||
|
||||
Each task follows this structure:
|
||||
|
||||
```markdown
|
||||
## Task [N]: [Short descriptive title]
|
||||
|
||||
**Description:** One paragraph explaining what this task accomplishes.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] [Specific, testable condition]
|
||||
- [ ] [Specific, testable condition]
|
||||
|
||||
**Verification:**
|
||||
- [ ] Tests pass (module-specific test command)
|
||||
- [ ] Lint/formatter passes (module-specific check command)
|
||||
|
||||
**Dependencies:** [Task numbers this depends on, or "None"]
|
||||
|
||||
**Files likely touched:**
|
||||
- `path/to/file.clj`
|
||||
- `path/to/file_test.clj`
|
||||
```
|
||||
|
||||
Replace "module-specific test command" with the actual commands for the module
|
||||
(e.g. `clojure -M:dev:test` for backend/common, `npx shadow-cljs compile test && npx karma start` for frontend,
|
||||
or the commands noted in the module's core memory).
|
||||
|
||||
#### Estimate Scope
|
||||
|
||||
| Size | Files | Scope |
|
||||
|------|-------|-------|
|
||||
| **XS** | 1 | Single function, config change, or schema tweak |
|
||||
| **S** | 1-2 | One handler or component method |
|
||||
| **M** | 3-5 | One vertical feature slice |
|
||||
| **L** | 5-8 | Multi-component feature |
|
||||
| **XL** | 8+ | **Too large — break it down further** |
|
||||
|
||||
If a task is L or larger, break it into smaller tasks. Agents perform best on
|
||||
S and M tasks.
|
||||
|
||||
**When to break a task down further:**
|
||||
- It would take more than one focused session
|
||||
- You cannot describe the acceptance criteria in 3 or fewer bullet points
|
||||
- It touches two or more independent subsystems
|
||||
- You find yourself writing "and" in the task title (a sign it is two tasks)
|
||||
|
||||
#### Order and Checkpoints
|
||||
|
||||
Arrange tasks so that:
|
||||
|
||||
1. Dependencies are satisfied (build foundation first)
|
||||
2. Each task leaves the system in a working state
|
||||
3. Verification checkpoints occur after every 2-3 tasks
|
||||
4. High-risk tasks are early (fail fast)
|
||||
|
||||
Add explicit checkpoints with the relevant module commands:
|
||||
|
||||
```markdown
|
||||
## Checkpoint: After Tasks 1-3
|
||||
- [ ] All tests pass (module-specific command)
|
||||
- [ ] Lint/format passes (module-specific command)
|
||||
- [ ] Core flow works end-to-end
|
||||
- [ ] Review with human before proceeding
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Analyze the codebase architecture and identify affected modules.
|
||||
- Read project conventions before drafting (start with `critical-info` and
|
||||
affected module core memories).
|
||||
- Break down complex features or bugs into atomic, actionable steps.
|
||||
- Propose solutions with clear rationale, trade-offs, and sequencing.
|
||||
- Identify risks, edge cases, performance implications, and breaking changes.
|
||||
- Apply DRY and KISS principles to the proposed implementation.
|
||||
- Define a testing strategy aligned with each affected module's tooling.
|
||||
- Every task must have acceptance criteria and verification steps.
|
||||
- Checkpoints must exist between major phases.
|
||||
|
||||
## Constraints
|
||||
|
||||
- You are **analysis-only** — never create, edit, or delete source code.
|
||||
- The only file write you may attempt is the plan itself, saved to
|
||||
`.opencode/plans/`.
|
||||
- You do **not** run builds, tests, linters, or any commands that modify state.
|
||||
- You do **not** create git commits or interact with version control.
|
||||
- You do **not** execute shell commands beyond read-only searches.
|
||||
- Your output is a structured plan or analysis, ready for handoff to an
|
||||
engineer agent or developer.
|
||||
|
||||
## Output Format
|
||||
|
||||
The plan is always delivered in the response so the user sees it regardless
|
||||
of which agent is running the skill.
|
||||
|
||||
Additionally, save the plan to:
|
||||
|
||||
```
|
||||
.opencode/plans/YYYY-MM-DD-<plan-one-line-title>.md
|
||||
```
|
||||
|
||||
Use today's date in the user's local timezone. The `<plan-one-line-title>`
|
||||
slug is lowercase, hyphen-separated, and a short summary of the task
|
||||
(e.g. `add-batch-get-profiles-for-file-comments`). Create the
|
||||
`.opencode/plans/` directory if it does not exist.
|
||||
|
||||
Always attempt the write. If the user explicitly provides a target file path,
|
||||
use that path instead of the default.
|
||||
|
||||
### Plan Document Template
|
||||
|
||||
```markdown
|
||||
# Plan: [Feature/Project Name]
|
||||
|
||||
## Context
|
||||
[One paragraph: what is the problem or feature request? Why is it needed?]
|
||||
|
||||
## Affected Modules
|
||||
[Which modules of the monorepo are involved? Reference module paths and any
|
||||
`mem:` memories that were consulted.]
|
||||
|
||||
## Architecture Decisions
|
||||
- [Key decision 1 and rationale]
|
||||
- [Key decision 2 and rationale]
|
||||
|
||||
## Risks & Considerations
|
||||
[Edge cases, performance implications, breaking changes, migration concerns,
|
||||
security implications.]
|
||||
|
||||
## Approach
|
||||
[Step-by-step implementation plan with file paths, function names, and code
|
||||
shape where applicable. Group steps into atomic, ordered tasks.]
|
||||
|
||||
## Task List
|
||||
|
||||
### Phase 1: Foundation
|
||||
- [ ] Task 1: ...
|
||||
- [ ] Task 2: ...
|
||||
|
||||
### Checkpoint: Phase 1
|
||||
- [ ] Tests pass, lint/formatter clean (module-specific commands)
|
||||
|
||||
### Phase 2: Core Features
|
||||
- [ ] Task 3: ...
|
||||
- [ ] Task 4: ...
|
||||
|
||||
### Checkpoint: Phase 2
|
||||
- [ ] End-to-end flow works
|
||||
|
||||
### Phase 3: Polish
|
||||
- [ ] Task 5: ...
|
||||
- [ ] Task 6: ...
|
||||
|
||||
### Checkpoint: Complete
|
||||
- [ ] All acceptance criteria met
|
||||
- [ ] Ready for review
|
||||
|
||||
## Testing Strategy
|
||||
[How to verify: which test commands to run per module, what cases to cover,
|
||||
manual verification steps, lint/format checks. Consult each module's core
|
||||
memory for the exact commands.]
|
||||
|
||||
## Parallelization Opportunities
|
||||
- **Safe to parallelize:** Independent feature slices across separate
|
||||
modules, tests for already-implemented features
|
||||
- **Must be sequential:** Shared common schema changes, database migrations
|
||||
- **Needs coordination:** Features that share a contract (define the contract
|
||||
first, then parallelize)
|
||||
|
||||
## Open Questions
|
||||
- [Question needing human input]
|
||||
```
|
||||
|
||||
When the plan is purely analytical (e.g. a code review or feasibility study
|
||||
with no implementation), skip the **Approach** and **Task List** sections and
|
||||
lead with **Findings** instead, keeping the rest of the structure.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before starting implementation, confirm:
|
||||
|
||||
- [ ] Every task has acceptance criteria
|
||||
- [ ] Every task has a verification step
|
||||
- [ ] Task dependencies are identified and ordered correctly
|
||||
- [ ] No task touches more than ~5 files
|
||||
- [ ] Checkpoints exist between major phases
|
||||
- [ ] The human has reviewed and approved the plan
|
||||
@ -1,134 +0,0 @@
|
||||
---
|
||||
name: refine-prompt
|
||||
description: Refine and improve a user-supplied prompt for maximum clarity and effectiveness using prompt-engineering best practices and Penpot project context. Outputs a rewritten prompt (and brief rationale); never executes the prompt.
|
||||
---
|
||||
|
||||
# Refine Prompt
|
||||
|
||||
Expert prompt-engineering pass on a user-supplied prompt. Takes a draft prompt
|
||||
and returns a clearer, more effective, well-structured version — ready to be
|
||||
used with any AI model. Never executes the prompt itself.
|
||||
|
||||
## When to Use
|
||||
|
||||
- The user shares a prompt and asks to improve, refine, polish, or rewrite it.
|
||||
- The user asks "make this prompt better" or "can you clean this up?".
|
||||
- The user wants to add structure, constraints, examples, or output format to
|
||||
a vague prompt.
|
||||
- The user wants a prompt adapted for a specific target model, audience, or
|
||||
task type.
|
||||
|
||||
Do **not** use this skill to actually answer the prompt or do the task — it
|
||||
only rewrites the prompt.
|
||||
|
||||
## Role
|
||||
|
||||
You are an expert Prompt Engineer with strong knowledge of Penpot. Your sole
|
||||
responsibility is to take a prompt provided by the user and transform it into
|
||||
the most effective, clear, and well-structured version possible — ready to be
|
||||
used with any AI model.
|
||||
|
||||
You do **not** execute tasks. You do **not** write code. You only design and
|
||||
refine prompts.
|
||||
|
||||
## Required Reading Before Refining
|
||||
|
||||
Before rewriting, internalize the project context the prompt will likely run
|
||||
against:
|
||||
|
||||
1. Read `AGENTS.md` (root) for the project-level rules and conventions.
|
||||
2. Read `.serena/memories/critical-info.md` (or the equivalent entry point) to
|
||||
understand the module layout (`frontend`, `backend`, `common`,
|
||||
`render-wasm`, `exporter`, `mcp`, `plugins`, `library`).
|
||||
3. Skim the relevant module's core memory (`mem:frontend/core`,
|
||||
`mem:backend/core`, etc.) when the prompt targets a specific module — this
|
||||
lets you inject precise vocabulary, file conventions, and test commands
|
||||
into the refined prompt.
|
||||
|
||||
This step matters most when the user is preparing a prompt *about* the
|
||||
Penpot codebase. For generic prompts, focus on prompt-engineering principles
|
||||
and only weave in Penpot context when it is clearly relevant.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Analyze the original prompt: identify its intent, target audience,
|
||||
ambiguities, missing context, and structural weaknesses.
|
||||
- Ask clarifying questions if the intent is unclear or if critical information
|
||||
is missing (e.g. target model, expected output format, tone, constraints).
|
||||
Keep questions concise and grouped. Prefer to ask 1–4 questions at once
|
||||
rather than one at a time. **Use the `question` tool** to ask them so the
|
||||
user gets a structured multi-choice UI; reserve a plain `## Clarifying
|
||||
questions` markdown section for cases where the `question` tool is
|
||||
unavailable or the question is genuinely open-ended.
|
||||
- Rewrite the prompt using prompt-engineering best practices (see below).
|
||||
- Preserve the user's original intent — do not change the underlying task.
|
||||
- When the user provides Penpot project context, weave in the relevant
|
||||
conventions, module paths, and tooling.
|
||||
|
||||
## Prompt Engineering Principles
|
||||
|
||||
Apply these techniques when refining prompts:
|
||||
|
||||
- **Be specific and explicit**: Replace vague instructions with precise ones.
|
||||
- **Set the context**: Include background information the model needs to
|
||||
perform well.
|
||||
- **Specify the output format**: State the desired structure, length, tone,
|
||||
or format (e.g. bullet list, JSON, step-by-step).
|
||||
- **Add constraints**: Include what the model should avoid or not do.
|
||||
- **Use examples** (few-shot): When applicable, suggest adding examples to
|
||||
anchor the model's behaviour.
|
||||
- **Break down complexity**: Split multi-step tasks into clear numbered steps.
|
||||
- **Avoid ambiguity**: Remove pronouns and references that could be
|
||||
misinterpreted.
|
||||
- **Chain of thought**: For reasoning tasks, include "Think step by step."
|
||||
- **Role framing**: When helpful, give the model a clear role
|
||||
("You are a senior backend engineer...").
|
||||
- **Tool awareness**: When the prompt targets an agentic model, mention
|
||||
relevant tools (`grep`, `glob`, `read`, `bash`, etc.) so the model uses the
|
||||
right surface.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do **not** execute the prompt yourself.
|
||||
- Do **not** answer the question inside the prompt.
|
||||
- Do **not** add unnecessary verbosity — prompts should be as short as they
|
||||
can be while remaining complete.
|
||||
- Always preserve the user's original intent.
|
||||
- If the user provides Penpot project context, prefer Penpot-specific
|
||||
vocabulary over generic terms (e.g. name actual modules and `mem:`
|
||||
references instead of "the codebase").
|
||||
|
||||
## Output Format
|
||||
|
||||
Deliver the result in the response as two clearly separated blocks:
|
||||
|
||||
1. **Refined prompt** — a single fenced code block (markdown ```) containing
|
||||
the rewritten prompt, ready to copy and use.
|
||||
2. **What changed (brief)** — a short bulleted list of the most important
|
||||
changes you made and why (3–7 bullets max). Skip the rationale if the
|
||||
changes are trivial.
|
||||
|
||||
If you asked clarifying questions via the `question` tool, stop and wait for
|
||||
the answers before producing a refined prompt. If the `question` tool was not
|
||||
available and you asked the questions in chat, list them in a separate
|
||||
**Clarifying questions** section above the refined prompt and stop — do not
|
||||
produce a refined prompt until the user answers. If the user explicitly told
|
||||
you to proceed without questions (e.g. "just rewrite it"), make reasonable
|
||||
assumptions and note them under **Assumptions made** in the rationale block.
|
||||
|
||||
## File Persistence
|
||||
|
||||
Always persist the refined prompt to disk so it can be re-used later, versioned
|
||||
in git, and shared with other agents. The response still contains the prompt
|
||||
and rationale blocks; the file is an additional artifact, not a replacement.
|
||||
|
||||
- Save the refined prompt (the body inside the fenced code block, **without**
|
||||
the surrounding ``` fences) to `.opencode/prompts/<descriptive-name>.md`.
|
||||
- Use a **kebab-case** filename that summarises the task, e.g.
|
||||
`add-error-reports-management-rpc.md`, `backend-rpc-security-audit.md`. No
|
||||
spaces, no uppercase, no version numbers or dates in the filename.
|
||||
- If `.opencode/prompts/` does not exist, create it before writing.
|
||||
- If a file with the same name already exists, overwrite it (the file is the
|
||||
refined prompt, not a log).
|
||||
- Only skip the file write when the user explicitly opts out (e.g. "don't save
|
||||
this one", "just show it in the chat"). When in doubt, save it.
|
||||
@ -1,457 +0,0 @@
|
||||
---
|
||||
name: security-and-hardening
|
||||
description: Hardens code against vulnerabilities. Use when handling user input, authentication, data storage, or external integrations. Use when building any feature that accepts untrusted data, manages user sessions, or interacts with third-party services.
|
||||
---
|
||||
|
||||
# Security and Hardening
|
||||
|
||||
## Overview
|
||||
|
||||
Security-first development practices for web applications. Treat every external input as hostile, every secret as sacred, and every authorization check as mandatory. Security isn't a phase — it's a constraint on every line of code that touches user data, authentication, or external systems.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Building anything that accepts user input
|
||||
- Implementing authentication or authorization
|
||||
- Storing or transmitting sensitive data
|
||||
- Integrating with external APIs or services
|
||||
- Adding file uploads, webhooks, or callbacks
|
||||
- Handling payment or PII data
|
||||
|
||||
## Process: Threat Model First
|
||||
|
||||
Controls bolted on without a threat model are guesses. Before hardening, spend five minutes thinking like an attacker:
|
||||
|
||||
1. **Map the trust boundaries.** Where does untrusted data cross into your system? HTTP requests, form fields, file uploads, webhooks, third-party APIs, message queues, and **LLM output**. Every boundary is attack surface.
|
||||
2. **Name the assets.** What's worth stealing or breaking? Credentials, PII, payment data, admin actions, money movement.
|
||||
3. **Run STRIDE over each boundary** — a quick lens, not a ceremony:
|
||||
|
||||
| Threat | Ask | Typical mitigation |
|
||||
|---|---|---|
|
||||
| **S**poofing | Can someone impersonate a user/service? | Authentication, signature verification |
|
||||
| **T**ampering | Can data be altered in transit or at rest? | Integrity checks, parameterized queries, HTTPS |
|
||||
| **R**epudiation | Can an action be denied later? | Audit logging of security events |
|
||||
| **I**nformation disclosure | Can data leak? | Encryption, field allowlists, generic errors |
|
||||
| **D**enial of service | Can it be overwhelmed? | Rate limiting, input size caps, timeouts |
|
||||
| **E**levation of privilege | Can a user gain rights they shouldn't? | Authorization checks, least privilege |
|
||||
|
||||
4. **Write abuse cases next to use cases.** For each feature, ask "how would I misuse this?" — then make that your first test.
|
||||
|
||||
If you can't name the trust boundaries for a feature, you're not ready to secure it. This is OWASP **A04: Insecure Design** — most breaches begin in design, not code.
|
||||
|
||||
## The Three-Tier Boundary System
|
||||
|
||||
### Always Do (No Exceptions)
|
||||
|
||||
- **Validate all external input** at the system boundary (API routes, form handlers)
|
||||
- **Parameterize all database queries** — never concatenate user input into SQL
|
||||
- **Encode output** to prevent XSS (use framework auto-escaping, don't bypass it)
|
||||
- **Use HTTPS** for all external communication
|
||||
- **Hash passwords** with bcrypt/scrypt/argon2 (never store plaintext)
|
||||
- **Set security headers** (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
|
||||
- **Use httpOnly, secure, sameSite cookies** for sessions
|
||||
- **Run `npm audit`** (or equivalent) before every release
|
||||
|
||||
### Ask First (Requires Human Approval)
|
||||
|
||||
- Adding new authentication flows or changing auth logic
|
||||
- Storing new categories of sensitive data (PII, payment info)
|
||||
- Adding new external service integrations
|
||||
- Changing CORS configuration
|
||||
- Adding file upload handlers
|
||||
- Modifying rate limiting or throttling
|
||||
- Granting elevated permissions or roles
|
||||
|
||||
### Never Do
|
||||
|
||||
- **Never commit secrets** to version control (API keys, passwords, tokens)
|
||||
- **Never log sensitive data** (passwords, tokens, full credit card numbers)
|
||||
- **Never trust client-side validation** as a security boundary
|
||||
- **Never disable security headers** for convenience
|
||||
- **Never use `eval()` or `innerHTML`** with user-provided data
|
||||
- **Never store sessions in client-accessible storage** (localStorage for auth tokens)
|
||||
- **Never expose stack traces** or internal error details to users
|
||||
|
||||
## OWASP Top 10 Prevention Patterns
|
||||
|
||||
These are prevention patterns, not a ranking. For the 2021 ordering, see the quick-reference table in `references/security-checklist.md`.
|
||||
|
||||
### Injection (SQL, NoSQL, OS Command)
|
||||
|
||||
```typescript
|
||||
// BAD: SQL injection via string concatenation
|
||||
const query = `SELECT * FROM users WHERE id = '${userId}'`;
|
||||
|
||||
// GOOD: Parameterized query
|
||||
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
||||
|
||||
// GOOD: ORM with parameterized input
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||
```
|
||||
|
||||
### Broken Authentication
|
||||
|
||||
```typescript
|
||||
// Password hashing
|
||||
import { hash, compare } from 'bcrypt';
|
||||
|
||||
const SALT_ROUNDS = 12;
|
||||
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
|
||||
const isValid = await compare(plaintext, hashedPassword);
|
||||
|
||||
// Session management
|
||||
app.use(session({
|
||||
secret: process.env.SESSION_SECRET, // From environment, not code
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
httpOnly: true, // Not accessible via JavaScript
|
||||
secure: true, // HTTPS only
|
||||
sameSite: 'lax', // CSRF protection
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
},
|
||||
}));
|
||||
```
|
||||
|
||||
### Cross-Site Scripting (XSS)
|
||||
|
||||
```typescript
|
||||
// BAD: Rendering user input as HTML
|
||||
element.innerHTML = userInput;
|
||||
|
||||
// GOOD: Use framework auto-escaping (React does this by default)
|
||||
return <div>{userInput}</div>;
|
||||
|
||||
// If you MUST render HTML, sanitize first
|
||||
import DOMPurify from 'dompurify';
|
||||
const clean = DOMPurify.sanitize(userInput);
|
||||
```
|
||||
|
||||
### Broken Access Control
|
||||
|
||||
```typescript
|
||||
// Always check authorization, not just authentication
|
||||
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
|
||||
const task = await taskService.findById(req.params.id);
|
||||
|
||||
// Check that the authenticated user owns this resource
|
||||
if (task.ownerId !== req.user.id) {
|
||||
return res.status(403).json({
|
||||
error: { code: 'FORBIDDEN', message: 'Not authorized to modify this task' }
|
||||
});
|
||||
}
|
||||
|
||||
// Proceed with update
|
||||
const updated = await taskService.update(req.params.id, req.body);
|
||||
return res.json(updated);
|
||||
});
|
||||
```
|
||||
|
||||
### Security Misconfiguration
|
||||
|
||||
```typescript
|
||||
// Security headers (use helmet for Express)
|
||||
import helmet from 'helmet';
|
||||
app.use(helmet());
|
||||
|
||||
// Content Security Policy
|
||||
app.use(helmet.contentSecurityPolicy({
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"], // Tighten if possible
|
||||
imgSrc: ["'self'", 'data:', 'https:'],
|
||||
connectSrc: ["'self'"],
|
||||
},
|
||||
}));
|
||||
|
||||
// CORS — restrict to known origins
|
||||
app.use(cors({
|
||||
origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
|
||||
credentials: true,
|
||||
}));
|
||||
```
|
||||
|
||||
### Sensitive Data Exposure
|
||||
|
||||
```typescript
|
||||
// Never return sensitive fields in API responses
|
||||
function sanitizeUser(user: UserRecord): PublicUser {
|
||||
const { passwordHash, resetToken, ...publicFields } = user;
|
||||
return publicFields;
|
||||
}
|
||||
|
||||
// Use environment variables for secrets
|
||||
const API_KEY = process.env.STRIPE_API_KEY;
|
||||
if (!API_KEY) throw new Error('STRIPE_API_KEY not configured');
|
||||
```
|
||||
|
||||
### Server-Side Request Forgery (SSRF)
|
||||
|
||||
Any time the server fetches a URL the user influenced — webhooks, "import from URL", image proxies, link previews — an attacker can aim it at internal services (cloud metadata, `localhost`, private IPs).
|
||||
|
||||
```typescript
|
||||
// BAD: fetch whatever the user gives you
|
||||
await fetch(req.body.webhookUrl);
|
||||
|
||||
// GOOD: allowlist scheme + host, reject if ANY resolved IP is private, forbid redirects
|
||||
import { lookup } from 'node:dns/promises';
|
||||
import ipaddr from 'ipaddr.js';
|
||||
|
||||
const ALLOWED_HOSTS = new Set(['hooks.example.com']);
|
||||
|
||||
async function assertSafeUrl(raw: string): Promise<URL> {
|
||||
const url = new URL(raw);
|
||||
if (url.protocol !== 'https:') throw new Error('https only');
|
||||
if (!ALLOWED_HOSTS.has(url.hostname)) throw new Error('host not allowed');
|
||||
// Resolve ALL records; a single private/reserved address fails the check.
|
||||
const addrs = await lookup(url.hostname, { all: true });
|
||||
if (addrs.some((a) => ipaddr.parse(a.address).range() !== 'unicast')) {
|
||||
throw new Error('private/reserved IP');
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
await fetch(await assertSafeUrl(req.body.webhookUrl), { redirect: 'error' });
|
||||
```
|
||||
|
||||
The `range() !== 'unicast'` check covers loopback, link-local `169.254.169.254` (cloud metadata, the #1 SSRF target), private, and unique-local ranges across IPv4 and IPv6.
|
||||
|
||||
**Caveat — this still has a TOCTOU gap.** `fetch` resolves DNS again after the check, so an attacker using a short-TTL record can rebind to an internal IP between validation and connection. For high-risk surfaces, resolve once and connect to the pinned IP, or put a filtering agent in front (`request-filtering-agent` / `ssrf-req-filter`).
|
||||
|
||||
## Input Validation Patterns
|
||||
|
||||
### Schema Validation at Boundaries
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const CreateTaskSchema = z.object({
|
||||
title: z.string().min(1).max(200).trim(),
|
||||
description: z.string().max(2000).optional(),
|
||||
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
||||
dueDate: z.string().datetime().optional(),
|
||||
});
|
||||
|
||||
// Validate at the route handler
|
||||
app.post('/api/tasks', async (req, res) => {
|
||||
const result = CreateTaskSchema.safeParse(req.body);
|
||||
if (!result.success) {
|
||||
return res.status(422).json({
|
||||
error: {
|
||||
code: 'VALIDATION_ERROR',
|
||||
message: 'Invalid input',
|
||||
details: result.error.flatten(),
|
||||
},
|
||||
});
|
||||
}
|
||||
// result.data is now typed and validated
|
||||
const task = await taskService.create(result.data);
|
||||
return res.status(201).json(task);
|
||||
});
|
||||
```
|
||||
|
||||
### File Upload Safety
|
||||
|
||||
```typescript
|
||||
// Restrict file types and sizes
|
||||
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
|
||||
function validateUpload(file: UploadedFile) {
|
||||
if (!ALLOWED_TYPES.includes(file.mimetype)) {
|
||||
throw new ValidationError('File type not allowed');
|
||||
}
|
||||
if (file.size > MAX_SIZE) {
|
||||
throw new ValidationError('File too large (max 5MB)');
|
||||
}
|
||||
// Don't trust the file extension — check magic bytes if critical
|
||||
}
|
||||
```
|
||||
|
||||
## Triaging npm audit Results
|
||||
|
||||
Not all audit findings require immediate action. Use this decision tree:
|
||||
|
||||
```
|
||||
npm audit reports a vulnerability
|
||||
├── Severity: critical or high
|
||||
│ ├── Is the vulnerable code reachable in your app?
|
||||
│ │ ├── YES --> Fix immediately (update, patch, or replace the dependency)
|
||||
│ │ └── NO (dev-only dep, unused code path) --> Fix soon, but not a blocker
|
||||
│ └── Is a fix available?
|
||||
│ ├── YES --> Update to the patched version
|
||||
│ └── NO --> Check for workarounds, consider replacing the dependency, or add to allowlist with a review date
|
||||
├── Severity: moderate
|
||||
│ ├── Reachable in production? --> Fix in the next release cycle
|
||||
│ └── Dev-only? --> Fix when convenient, track in backlog
|
||||
└── Severity: low
|
||||
└── Track and fix during regular dependency updates
|
||||
```
|
||||
|
||||
**Key questions:**
|
||||
- Is the vulnerable function actually called in your code path?
|
||||
- Is the dependency a runtime dependency or dev-only?
|
||||
- Is the vulnerability exploitable given your deployment context (e.g., a server-side vulnerability in a client-only app)?
|
||||
|
||||
When you defer a fix, document the reason and set a review date.
|
||||
|
||||
### Supply-Chain Hygiene
|
||||
|
||||
`npm audit` catches known CVEs; it won't catch a malicious or typosquatted package. Also:
|
||||
|
||||
- **Commit the lockfile** and install with `npm ci` (not `npm install`) in CI — reproducible builds, no silent version drift.
|
||||
- **Review new dependencies before adding them** — maintenance, download counts, and whether they truly earn their place. Every dependency is attack surface (OWASP **A06: Vulnerable Components**, **LLM03: Supply Chain**).
|
||||
- **Be wary of `postinstall` scripts** in unfamiliar packages — they run arbitrary code at install time.
|
||||
- **Watch for typosquats** — `cross-env` vs `crossenv`, `react-dom` vs `reactdom`.
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
// General API rate limit
|
||||
app.use('/api/', rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // 100 requests per window
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
}));
|
||||
|
||||
// Stricter limit for auth endpoints
|
||||
app.use('/api/auth/', rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 10, // 10 attempts per 15 minutes
|
||||
}));
|
||||
```
|
||||
|
||||
## Secrets Management
|
||||
|
||||
```
|
||||
.env files:
|
||||
├── .env.example → Committed (template with placeholder values)
|
||||
├── .env → NOT committed (contains real secrets)
|
||||
└── .env.local → NOT committed (local overrides)
|
||||
|
||||
.gitignore must include:
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.pem
|
||||
*.key
|
||||
```
|
||||
|
||||
**Always check before committing:**
|
||||
```bash
|
||||
# Check for accidentally staged secrets
|
||||
git diff --cached | grep -i "password\|secret\|api_key\|token"
|
||||
```
|
||||
|
||||
**If a secret is ever committed, rotate it.** Deleting the line or rewriting history is not enough — assume it's compromised the moment it reaches a remote. Revoke and reissue the key first, then purge it from history.
|
||||
|
||||
## Securing AI / LLM Features
|
||||
|
||||
If your app calls an LLM — chatbots, summarizers, agents, RAG — it inherits a new attack surface. Map it to the [OWASP Top 10 for LLM Applications (2025)](https://genai.owasp.org/llm-top-10/):
|
||||
|
||||
- **Treat all model output as untrusted input (LLM05: Improper Output Handling).** Never pass LLM output straight into `eval`, SQL, a shell, `innerHTML`, or a file path. Validate and encode it exactly as you would raw user input.
|
||||
- **Assume prompts can be hijacked (LLM01: Prompt Injection).** Untrusted text in the context window — a user message, a fetched web page, a PDF — can carry instructions. The system prompt is not a security boundary; enforce permissions in code, not in the prompt.
|
||||
- **Keep secrets and other users' data out of prompts (LLM02 / LLM07).** Anything in the context can be echoed back. Don't put API keys, cross-tenant data, or the full system prompt where the model can repeat it.
|
||||
- **Constrain tool and agent permissions (LLM06: Excessive Agency).** Scope tools to the minimum, require confirmation for destructive or irreversible actions, and validate every tool argument.
|
||||
- **Bound consumption (LLM10: Unbounded Consumption).** Cap tokens, request rate, and loop/recursion depth so a crafted input can't run up cost or hang the system.
|
||||
- **Isolate retrieval data (LLM08: Vector and Embedding Weaknesses).** In RAG, treat the vector store as a trust boundary: partition embeddings per tenant so one user can't retrieve another's data, and validate documents before indexing so poisoned content can't steer answers.
|
||||
|
||||
```typescript
|
||||
// BAD: trusting model output as a command or as markup
|
||||
const sql = await llm.generate(`Write SQL for: ${userQuestion}`);
|
||||
await db.query(sql); // arbitrary query execution
|
||||
container.innerHTML = await llm.reply(userMessage); // stored XSS, via the model
|
||||
|
||||
// GOOD: model output is data — parse defensively, then validate, then encode
|
||||
let intent;
|
||||
try {
|
||||
intent = CommandSchema.parse(JSON.parse(await llm.replyJson(userMessage)));
|
||||
} catch {
|
||||
throw new ValidationError('unexpected model output'); // JSON.parse or schema failed
|
||||
}
|
||||
await runAllowlistedAction(intent.action, intent.params);
|
||||
container.textContent = await llm.reply(userMessage);
|
||||
```
|
||||
|
||||
## Security Review Checklist
|
||||
|
||||
```markdown
|
||||
### Authentication
|
||||
- [ ] Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12)
|
||||
- [ ] Session tokens are httpOnly, secure, sameSite
|
||||
- [ ] Login has rate limiting
|
||||
- [ ] Password reset tokens expire
|
||||
|
||||
### Authorization
|
||||
- [ ] Every endpoint checks user permissions
|
||||
- [ ] Users can only access their own resources
|
||||
- [ ] Admin actions require admin role verification
|
||||
|
||||
### Input
|
||||
- [ ] All user input validated at the boundary
|
||||
- [ ] SQL queries are parameterized
|
||||
- [ ] HTML output is encoded/escaped
|
||||
- [ ] Server-side URL fetches are allowlisted (no SSRF to internal services)
|
||||
|
||||
### Data
|
||||
- [ ] No secrets in code or version control
|
||||
- [ ] Sensitive fields excluded from API responses
|
||||
- [ ] PII encrypted at rest (if applicable)
|
||||
|
||||
### Infrastructure
|
||||
- [ ] Security headers configured (CSP, HSTS, etc.)
|
||||
- [ ] CORS restricted to known origins
|
||||
- [ ] Dependencies audited for vulnerabilities
|
||||
- [ ] Error messages don't expose internals
|
||||
|
||||
### Supply Chain
|
||||
- [ ] Lockfile committed; CI installs with `npm ci`
|
||||
- [ ] New dependencies reviewed (maintenance, downloads, postinstall scripts)
|
||||
|
||||
### AI / LLM (if used)
|
||||
- [ ] Model output treated as untrusted (no eval/SQL/innerHTML/shell)
|
||||
- [ ] Secrets and other users' data kept out of prompts
|
||||
- [ ] Tool/agent permissions scoped; destructive actions require confirmation
|
||||
```
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "This is an internal tool, security doesn't matter" | Internal tools get compromised. Attackers target the weakest link. |
|
||||
| "We'll add security later" | Security retrofitting is 10x harder than building it in. Add it now. |
|
||||
| "No one would try to exploit this" | Automated scanners will find it. Security by obscurity is not security. |
|
||||
| "The framework handles security" | Frameworks provide tools, not guarantees. You still need to use them correctly. |
|
||||
| "It's just a prototype" | Prototypes become production. Security habits from day one. |
|
||||
| "Threat modeling is overkill here" | Five minutes of "how would I attack this?" prevents the design flaws no control can patch later. |
|
||||
| "It's just LLM output, it's only text" | That "text" can be a SQL statement, a script tag, or a shell command. Treat it like any untrusted input. |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- User input passed directly to database queries, shell commands, or HTML rendering
|
||||
- Secrets in source code or commit history
|
||||
- API endpoints without authentication or authorization checks
|
||||
- Missing CORS configuration or wildcard (`*`) origins
|
||||
- No rate limiting on authentication endpoints
|
||||
- Stack traces or internal errors exposed to users
|
||||
- Dependencies with known critical vulnerabilities
|
||||
- Server fetches user-supplied URLs without an allowlist (SSRF)
|
||||
- LLM/model output passed into a query, the DOM, a shell, or `eval`
|
||||
- Secrets, PII, or the full system prompt placed inside an LLM context window
|
||||
|
||||
## Verification
|
||||
|
||||
After implementing security-relevant code:
|
||||
|
||||
- [ ] `npm audit` shows no critical or high vulnerabilities
|
||||
- [ ] No secrets in source code or git history
|
||||
- [ ] All user input validated at system boundaries
|
||||
- [ ] Authentication and authorization checked on every protected endpoint
|
||||
- [ ] Security headers present in response (check with browser DevTools)
|
||||
- [ ] Error responses don't expose internal details
|
||||
- [ ] Rate limiting active on auth endpoints
|
||||
- [ ] Server-side URL fetches validated against an allowlist (no SSRF)
|
||||
- [ ] LLM/model output validated and encoded before use (if AI features present)
|
||||
@ -13,7 +13,7 @@ Fetch information from Taiga public API for the **Penpot** project
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `python3` — the `scripts/taiga.py` CLI script is self-contained (stdlib only)
|
||||
- `python3` — the `tools/taiga.py` CLI script is self-contained (stdlib only)
|
||||
|
||||
## Quick Start
|
||||
|
||||
@ -21,17 +21,17 @@ The easiest way is to use the bundled Python script:
|
||||
|
||||
```bash
|
||||
# Pass a Taiga URL directly
|
||||
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
||||
python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
||||
|
||||
# Or use "<type> <ref>" syntax
|
||||
python3 scripts/taiga.py us 14128
|
||||
python3 scripts/taiga.py task 13648
|
||||
python3 tools/taiga.py us 14128
|
||||
python3 tools/taiga.py task 13648
|
||||
|
||||
# Add --json for raw output
|
||||
python3 scripts/taiga.py --json issue 13714
|
||||
python3 tools/taiga.py --json issue 13714
|
||||
|
||||
# See full usage
|
||||
python3 scripts/taiga.py --help
|
||||
python3 tools/taiga.py --help
|
||||
```
|
||||
|
||||
## URL Pattern Reference
|
||||
@ -51,30 +51,30 @@ To extract the **type** and **ref** from a URL:
|
||||
|
||||
## Python Script Reference
|
||||
|
||||
The `scripts/taiga.py` script wraps the Taiga API into a single convenient CLI
|
||||
The `tools/taiga.py` script wraps the Taiga API into a single convenient CLI
|
||||
with sensible defaults.
|
||||
|
||||
### Usage
|
||||
|
||||
```
|
||||
python3 scripts/taiga.py <taiga-url>
|
||||
python3 scripts/taiga.py <type> <ref>
|
||||
python3 scripts/taiga.py [--json] <taiga-url>
|
||||
python3 scripts/taiga.py [--json] <type> <ref>
|
||||
python3 tools/taiga.py <taiga-url>
|
||||
python3 tools/taiga.py <type> <ref>
|
||||
python3 tools/taiga.py [--json] <taiga-url>
|
||||
python3 tools/taiga.py [--json] <type> <ref>
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# By URL (recommended — no need to think about type/ref)
|
||||
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
||||
python3 tools/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
||||
|
||||
# By type and ref
|
||||
python3 scripts/taiga.py us 14128
|
||||
python3 scripts/taiga.py task 13648
|
||||
python3 tools/taiga.py us 14128
|
||||
python3 tools/taiga.py task 13648
|
||||
|
||||
# Raw JSON output
|
||||
python3 scripts/taiga.py --json issue 13714
|
||||
python3 tools/taiga.py --json issue 13714
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
@ -20,7 +20,7 @@ primary link, with the fix PR inline on the same line.
|
||||
|
||||
- `gh` CLI authenticated (`gh auth status`)
|
||||
- Python 3.8+
|
||||
- `scripts/gh.py` helper script available
|
||||
- `tools/gh.py` helper script available
|
||||
|
||||
## Workflow
|
||||
|
||||
@ -36,19 +36,19 @@ Use the helper script. It uses GraphQL for efficient single-pass fetching
|
||||
|
||||
```bash
|
||||
# All closed issues (default)
|
||||
python3 scripts/gh.py issues "2.16.0"
|
||||
python3 tools/gh.py issues "2.16.0"
|
||||
|
||||
# Include open issues too
|
||||
python3 scripts/gh.py issues "2.16.0" --state all
|
||||
python3 tools/gh.py issues "2.16.0" --state all
|
||||
|
||||
# Exclude entries that should not go in the changelog
|
||||
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
|
||||
python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
|
||||
```
|
||||
|
||||
**Exclusion rules (issue-level):**
|
||||
- `no changelog` label — Chore/refactor work that doesn't need a changelog entry
|
||||
- `release blocker` label — Blocked issues not yet ready for changelog
|
||||
- `Task` issue type — Internal chores are not user-facing; automatically excluded by `gh.py`. Use `--include-tasks` to override.
|
||||
- `Task` issue type — Internal chores are not user-facing; filter these out after fetching
|
||||
- **Rejected project status** — Issues with a "Rejected" status in the "Main" project board are automatically excluded by `gh.py`. This project-level status (independent of the GitHub issue `state`) indicates the issue was rejected from the release. Use `--include-rejected` to override.
|
||||
|
||||
**Exclusion rules (PR-level):**
|
||||
@ -68,7 +68,7 @@ If updating from an existing `CHANGES.md`, find issues in the milestone that
|
||||
are NOT yet referenced in the changelog:
|
||||
|
||||
```bash
|
||||
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog" --compare CHANGES.md
|
||||
python3 tools/gh.py issues "2.16.0" --exclude "release blocker,no changelog" --compare CHANGES.md
|
||||
```
|
||||
|
||||
This returns a filtered JSON array with only the missing issues.
|
||||
@ -85,23 +85,23 @@ community contribution attribution, or to read the PR body for
|
||||
|
||||
```bash
|
||||
# One or more PR numbers
|
||||
python3 scripts/gh.py prs 9179 9204 9311
|
||||
python3 tools/gh.py prs 9179 9204 9311
|
||||
|
||||
# From a file
|
||||
python3 scripts/gh.py prs --file prs.txt
|
||||
python3 tools/gh.py prs --file prs.txt
|
||||
|
||||
# From stdin
|
||||
cat prs.txt | python3 scripts/gh.py prs --stdin
|
||||
cat prs.txt | python3 tools/gh.py prs --stdin
|
||||
```
|
||||
|
||||
The `prs` command also supports listing all PRs in a milestone in one call:
|
||||
|
||||
```bash
|
||||
# All merged PRs in a milestone (default)
|
||||
python3 scripts/gh.py prs --milestone "2.16.0"
|
||||
python3 tools/gh.py prs --milestone "2.16.0"
|
||||
|
||||
# All states (merged, open, closed)
|
||||
python3 scripts/gh.py prs --milestone "2.16.0" --state all
|
||||
python3 tools/gh.py prs --milestone "2.16.0" --state all
|
||||
```
|
||||
|
||||
The `prs` command returns JSON with `number`, `title`, `body`, `state`,
|
||||
@ -113,13 +113,13 @@ You can also list all PRs in a milestone in a single call:
|
||||
|
||||
```bash
|
||||
# All merged PRs in a milestone (default)
|
||||
python3 scripts/gh.py prs --milestone "2.16.0"
|
||||
python3 tools/gh.py prs --milestone "2.16.0"
|
||||
|
||||
# All states (merged, open, closed)
|
||||
python3 scripts/gh.py prs --milestone "2.16.0" --state all
|
||||
python3 tools/gh.py prs --milestone "2.16.0" --state all
|
||||
|
||||
# Open PRs only
|
||||
python3 scripts/gh.py prs --milestone "2.16.0" --state open
|
||||
python3 tools/gh.py prs --milestone "2.16.0" --state open
|
||||
```
|
||||
|
||||
The milestone path uses paginated GraphQL on the milestone's `pullRequests`
|
||||
@ -147,12 +147,6 @@ belongs to.
|
||||
The `gh.py` issues command already includes `issue_type` in every entry's
|
||||
output. **No separate GraphQL query is needed.**
|
||||
|
||||
**Preserve highlighted entries:** If an entry is already featured in
|
||||
`### :rocket: Epics and highlights`, keep it in that section when refreshing a
|
||||
changelog version. Do not remove a highlighted entry just because issue type
|
||||
categorization would otherwise place it under `### :sparkles: New features &
|
||||
Enhancements`.
|
||||
|
||||
**Community contribution attribution:** If the issue or its fix PR has the
|
||||
`community contribution` label, add an attribution `(by @<github_username>)`
|
||||
on the changelog entry line, **before** the GitHub issue/PR references.
|
||||
@ -161,7 +155,7 @@ The attribution should reference the **PR author**, not the issue author.
|
||||
The `prs` subcommand includes the `author` field — use that:
|
||||
|
||||
```bash
|
||||
python3 scripts/gh.py prs <PR_NUMBER> | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])"
|
||||
python3 tools/gh.py prs <PR_NUMBER> | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['author'])"
|
||||
```
|
||||
|
||||
Placement in the entry line:
|
||||
@ -196,7 +190,7 @@ only reference **merged** PRs. Verify before writing:
|
||||
|
||||
```bash
|
||||
# Collect all PR numbers from the candidate entries and check them
|
||||
python3 scripts/gh.py prs <ALL_PR_NUMBERS> | python3 -c "
|
||||
python3 tools/gh.py prs <ALL_PR_NUMBERS> | python3 -c "
|
||||
import json, sys
|
||||
for pr in json.load(sys.stdin):
|
||||
if pr['state'] != 'MERGED':
|
||||
@ -247,65 +241,6 @@ Format details:
|
||||
- When an entry already exists in an earlier version section, it must be removed
|
||||
from the current version to avoid duplicates
|
||||
|
||||
### 6a. Pre-flight checks — fix rule violations in the changelog
|
||||
|
||||
**The LLM must apply these checks during the workflow and fix any
|
||||
violations directly in `CHANGES.md`. They are not anomalies — they are
|
||||
process errors that should be corrected before writing the new section.**
|
||||
|
||||
The changelog is a *snapshot* of the milestone at a point in time, but
|
||||
milestones and changelog entries can drift. The LLM must reconcile the
|
||||
existing changelog against the current state of the milestone and the
|
||||
existing changelog entries.
|
||||
|
||||
For each entry that already exists in `CHANGES.md` (in any version
|
||||
section) or in the candidate set for the current milestone, check:
|
||||
|
||||
1. **Duplicate across versions.** Is the same issue already documented
|
||||
in another (older) version section? If yes, this is a *backport*:
|
||||
- The user-facing fix was already released. Remove the duplicate
|
||||
from the current section. The earlier version is the canonical
|
||||
reference.
|
||||
|
||||
2. **Stale milestone assignment.** Has the issue been moved out of the
|
||||
current milestone since the changelog was last updated (e.g., a fix
|
||||
arrived late and the issue was reassigned to a future milestone)?
|
||||
- Verify the issue is still in the current milestone via
|
||||
`python3 scripts/gh.py issues <MILESTONE> --state all`. If it's no
|
||||
longer there, remove the entry from the current section. (If the
|
||||
target section doesn't exist yet, the entry is simply dropped.)
|
||||
|
||||
3. **Exclusion labels newly applied.** Did the issue acquire a
|
||||
`no changelog` or `release blocker` label since the changelog was
|
||||
last updated? If yes, remove the entry from the current section.
|
||||
|
||||
4. **Issue state changed.** Is the issue still closed? Has it been
|
||||
reopened, deleted, or moved to a `Rejected` project status? If yes,
|
||||
remove the entry.
|
||||
|
||||
5. **Unmerged or removed PR references.** For every PR referenced in
|
||||
the entry, is the PR still merged? Was the PR closed without
|
||||
merging (superseded)? Was the PR moved to a different milestone?
|
||||
If the only referenced PR is no longer merged, fix the reference
|
||||
(find the actual merged fix PR) or remove the entry. A PR that is
|
||||
merged in a *different* milestone is reported as an anomaly in
|
||||
step 11 — do not silently remove it.
|
||||
|
||||
6. **Issue type changed.** Did the issue type change (e.g., from Bug to
|
||||
Task)? If the new type is `Task`, the issue is internal and should
|
||||
be removed.
|
||||
|
||||
7. **Cross-section completeness.** For every closed, non-excluded
|
||||
milestone issue that is *not* referenced in any version section of
|
||||
the changelog, add it to the current section (per the categorization
|
||||
rules in step 5).
|
||||
|
||||
After these checks, the changelog should be internally consistent with
|
||||
the milestone. **Do not defer these fixes to step 11 — they are
|
||||
workflow errors, not anomalies.** Step 11 only reports milestone
|
||||
mismatches that require human judgment about the team's release
|
||||
intent.
|
||||
|
||||
### 7. Build the description text
|
||||
|
||||
Derive the description from the issue title, not the PR title. Strip leading
|
||||
@ -343,7 +278,7 @@ cross-reference to catch gaps:
|
||||
|
||||
```bash
|
||||
# List all merged PRs in the milestone
|
||||
python3 scripts/gh.py prs --milestone "<MILESTONE>" --state merged > /tmp/milestone-prs.json
|
||||
python3 tools/gh.py prs --milestone "<MILESTONE>" --state merged > /tmp/milestone-prs.json
|
||||
|
||||
# Extract PR numbers from the changelog section
|
||||
python3 -c "
|
||||
@ -384,7 +319,7 @@ changelog or is legitimately excluded (check its labels).
|
||||
Also verify that no closed-unmerged PRs remain in the changelog:
|
||||
|
||||
```bash
|
||||
python3 scripts/gh.py prs --milestone "<MILESTONE>" --state all | python3 -c "
|
||||
python3 tools/gh.py prs --milestone "<MILESTONE>" --state all | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
closed = [p for p in data if p['state'] == 'CLOSED']
|
||||
@ -412,291 +347,71 @@ if closed:
|
||||
- <fix description> (by @contributor) [#<ISSUE>](https://github.com/penpot/penpot/issues/<ISSUE>) (PR: [#<PR>](https://github.com/penpot/penpot/pull/<PR>))
|
||||
```
|
||||
|
||||
### 11. Generate anomaly report and save to CHANGES-ISSUES.md
|
||||
### 10. Cross-reference milestone PRs against the changelog
|
||||
|
||||
After all edits and cross-referencing are complete, generate a structured
|
||||
report and save it to `CHANGES-ISSUES.md` (overwriting if exists).
|
||||
This provides a persistent record of any discrepancies between the milestone
|
||||
and the changelog.
|
||||
|
||||
**Every issue and PR number in the report must be rendered as a full GitHub
|
||||
Markdown link** using the same URL format as `CHANGES.md`:
|
||||
- Issue N → `[#N](https://github.com/penpot/penpot/issues/N)`
|
||||
- PR N → `[#N](https://github.com/penpot/penpot/pull/N)`
|
||||
|
||||
The titles and notes should also link to the corresponding issue/PR page
|
||||
where applicable, so the report is self-contained and clickable from any
|
||||
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:
|
||||
|
||||
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
|
||||
release, but the PR is being released elsewhere — the fix may not
|
||||
actually ship here.
|
||||
2. **PR is in the milestone, but the issue it closes is in a different
|
||||
milestone (or has no milestone).** The PR is being released here, but
|
||||
the issue it fixes is being released in a different version (or never
|
||||
tracked in a milestone) — the changelog pairing is misleading.
|
||||
|
||||
**Anything else is not an anomaly.** Other discrepancies (exclusion
|
||||
labels on in-changelog issues, missing valid issues, unmerged PR
|
||||
references, duplicates across versions, stale milestone assignments)
|
||||
are **rule violations** that the LLM must fix directly in `CHANGES.md`
|
||||
during step 6a (pre-flight checks). They should not appear in this
|
||||
report — if they do, the LLM has skipped the pre-flight step and
|
||||
needs to re-run the workflow.
|
||||
|
||||
The changelog's primary unit is the **issue**, not the PR, so a missing or
|
||||
mismatched PR only matters when its issue is part of this milestone.
|
||||
|
||||
Run this self-contained script:
|
||||
Issues can be fixed by PRs that aren't in the milestone, and merged PRs in
|
||||
the milestone may not close any tracked issue. After writing, run a full
|
||||
cross-reference to catch gaps:
|
||||
|
||||
```bash
|
||||
python3 << 'PYEOF'
|
||||
import json, re, subprocess, sys
|
||||
from datetime import datetime, timezone
|
||||
# List all merged PRs in the milestone
|
||||
python3 tools/gh.py prs --milestone "<MILESTONE>" --state merged > /tmp/milestone-prs.json
|
||||
|
||||
MILESTONE = "<MILESTONE>"
|
||||
CHANGES_MD = "CHANGES.md"
|
||||
OUTPUT = "CHANGES-ISSUES.md"
|
||||
REPO = "penpot/penpot"
|
||||
# Extract PR numbers from the changelog section
|
||||
python3 -c "
|
||||
import json, re
|
||||
|
||||
# --- URL helpers (match CHANGES.md format exactly) ---
|
||||
def issue_url(n): return f"https://github.com/{REPO}/issues/{n}"
|
||||
def pr_url(n): return f"https://github.com/{REPO}/pull/{n}"
|
||||
def issue_link(n): return f"[#{n}]({issue_url(n)})"
|
||||
def pr_link(n): return f"[#{n}]({pr_url(n)})"
|
||||
def issue_link_title(n, title):
|
||||
url = issue_url(n)
|
||||
if title:
|
||||
return f"[#{n}]({url}) — [{title}]({url})"
|
||||
return f"[#{n}]({url})"
|
||||
def pr_link_title(n, title):
|
||||
url = pr_url(n)
|
||||
if title:
|
||||
return f"[#{n}]({url}) — [{title}]({url})"
|
||||
return f"[#{n}]({url})"
|
||||
def fmt_pr_list(nums):
|
||||
return ", ".join(pr_link(n) for n in nums)
|
||||
def fmt_issue_list(nums):
|
||||
return ", ".join(issue_link(n) for n in nums)
|
||||
|
||||
# --- Fetch milestone data ---
|
||||
result = subprocess.run(
|
||||
["python3", "scripts/gh.py", "issues", MILESTONE, "--state", "all"],
|
||||
capture_output=True, text=True)
|
||||
all_issues = json.loads(result.stdout)
|
||||
issue_by_num = {i['number']: i for i in all_issues}
|
||||
|
||||
result = subprocess.run(
|
||||
["python3", "scripts/gh.py", "prs", "--milestone", MILESTONE, "--state", "all"],
|
||||
capture_output=True, text=True)
|
||||
all_prs = json.loads(result.stdout)
|
||||
pr_by_num = {p['number']: p for p in all_prs}
|
||||
|
||||
# --- Read changelog section ---
|
||||
with open(CHANGES_MD) as f:
|
||||
with open('CHANGES.md') as f:
|
||||
content = f.read()
|
||||
|
||||
m = re.search(rf'## {re.escape(MILESTONE)}(?:\s*\([^)]*\))?\n(.*?)(?:\n## |\Z)', content, re.DOTALL)
|
||||
section = m.group(1) if m else ""
|
||||
|
||||
changelog_issues = set()
|
||||
for num in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/issues/\d+\)', section):
|
||||
changelog_issues.add(int(num))
|
||||
for num in re.findall(r'\[Github #(\d+)\]', section):
|
||||
changelog_issues.add(int(num))
|
||||
# Extract the version section (adjust regex to match the actual version)
|
||||
match = re.search(r'## <MILESTONE> \(Unreleased\)\n(.*?)(?:\n## |\Z)', content, re.DOTALL)
|
||||
section = match.group(1)
|
||||
|
||||
# Collect all PR numbers referenced
|
||||
changelog_prs = set()
|
||||
for num in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)', section):
|
||||
changelog_prs.add(int(num))
|
||||
for num in re.findall(r'PR:\[(\d+)\]', section):
|
||||
changelog_prs.add(int(num))
|
||||
for m in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)', section):
|
||||
changelog_prs.add(int(m))
|
||||
|
||||
# --- Milestone lookup caches ---
|
||||
# PRs and issues returned by milestone queries are KNOWN to be in MILESTONE.
|
||||
# For everything else, fall back to `gh` per-item lookups.
|
||||
pr_milestone_cache = {p['number']: MILESTONE for p in all_prs}
|
||||
issue_milestone_cache = {i['number']: MILESTONE for i in all_issues}
|
||||
# Collect all milestone PRs (filtered)
|
||||
with open('/tmp/milestone-prs.json') as f:
|
||||
milestone_prs = json.load(f)
|
||||
|
||||
def get_pr_milestone(pr_num):
|
||||
"""Return the milestone title for a PR, or None if unassigned / unknown."""
|
||||
if pr_num in pr_milestone_cache:
|
||||
return pr_milestone_cache[pr_num]
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["gh", "pr", "view", str(pr_num), "--json", "milestone"],
|
||||
capture_output=True, text=True, check=True)
|
||||
data = json.loads(r.stdout)
|
||||
ms = data.get('milestone')
|
||||
pr_milestone_cache[pr_num] = (ms or {}).get('title')
|
||||
except (subprocess.CalledProcessError, json.JSONDecodeError):
|
||||
pr_milestone_cache[pr_num] = None
|
||||
return pr_milestone_cache[pr_num]
|
||||
milestone_merged = {pr['number'] for pr in milestone_prs}
|
||||
|
||||
def get_issue_milestone(issue_num):
|
||||
"""Return the milestone title for an issue, or None if unassigned / unknown."""
|
||||
if issue_num in issue_milestone_cache:
|
||||
return issue_milestone_cache[issue_num]
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["gh", "issue", "view", str(issue_num), "--json", "milestone"],
|
||||
capture_output=True, text=True, check=True)
|
||||
data = json.loads(r.stdout)
|
||||
ms = data.get('milestone')
|
||||
issue_milestone_cache[issue_num] = (ms or {}).get('title')
|
||||
except (subprocess.CalledProcessError, json.JSONDecodeError):
|
||||
issue_milestone_cache[issue_num] = None
|
||||
return issue_milestone_cache[issue_num]
|
||||
|
||||
# --- Exclusion rules (shared) ---
|
||||
EXCLUDED_LABELS = {'release blocker', 'no changelog'}
|
||||
EXCLUDED_ISSUE_TYPES = {'Task'}
|
||||
EXCLUDED_PROJECT_STATUS = {'Rejected'}
|
||||
|
||||
def issue_excluded(issue):
|
||||
if not issue: return True
|
||||
if issue.get('state') != 'CLOSED': return True
|
||||
if issue.get('issue_type') in EXCLUDED_ISSUE_TYPES: return True
|
||||
if issue.get('project_status') in EXCLUDED_PROJECT_STATUS: return True
|
||||
if EXCLUDED_LABELS & set(issue.get('labels', [])): return True
|
||||
return False
|
||||
|
||||
# --- ANOMALIES: milestone mismatches between issues and their referenced PRs ---
|
||||
# These are the ONLY items that should appear in the report. All other
|
||||
# discrepancies (exclusion labels, missing valid issues, unmerged PRs,
|
||||
# duplicates, stale milestone assignments) are workflow errors that the
|
||||
# LLM must fix in step 6a (pre-flight checks) — they are not anomalies.
|
||||
|
||||
# Type A: issue in MILESTONE, referenced PR in different milestone or no milestone
|
||||
anomalies_a = [] # list of dicts: {issue, issue_title, pr, pr_milestone}
|
||||
for issue_num in sorted(changelog_issues):
|
||||
issue = issue_by_num.get(issue_num)
|
||||
if not issue: continue
|
||||
if get_issue_milestone(issue_num) != MILESTONE: continue
|
||||
for pr_num in issue.get('closing_prs', []):
|
||||
pr_ms = get_pr_milestone(pr_num)
|
||||
if pr_ms != MILESTONE:
|
||||
anomalies_a.append({
|
||||
'issue': issue_num,
|
||||
'issue_title': issue.get('title', ''),
|
||||
'pr': pr_num,
|
||||
'pr_milestone': pr_ms, # may be None
|
||||
})
|
||||
|
||||
# Type B: PR in MILESTONE, the issue it closes is in different milestone or no milestone
|
||||
anomalies_b = [] # list of dicts: {pr, pr_title, issue, issue_milestone}
|
||||
for pr_num in sorted(changelog_prs):
|
||||
pr = pr_by_num.get(pr_num)
|
||||
if not pr: continue
|
||||
if get_pr_milestone(pr_num) != MILESTONE: continue
|
||||
for issue_num in pr.get('closing_issues', []):
|
||||
issue_ms = get_issue_milestone(issue_num)
|
||||
if issue_ms != MILESTONE:
|
||||
anomalies_b.append({
|
||||
'pr': pr_num,
|
||||
'pr_title': pr.get('title', ''),
|
||||
'issue': issue_num,
|
||||
'issue_milestone': issue_ms, # may be None
|
||||
})
|
||||
|
||||
# --- Write report ---
|
||||
def fmt_ms(ms):
|
||||
return ms if ms else "_none_"
|
||||
|
||||
with open(OUTPUT, 'w') as f:
|
||||
f.write(f'# Changelog Anomaly Report — {MILESTONE}\n\n')
|
||||
f.write(f'Generated: {datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")}\n\n')
|
||||
f.write('---\n\n')
|
||||
|
||||
n_a = len(anomalies_a)
|
||||
n_b = len(anomalies_b)
|
||||
|
||||
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 different milestone or no milestone:** {n_b}\n')
|
||||
f.write(f'- **Total anomalies:** {n_a + n_b}\n\n')
|
||||
|
||||
# --- Anomalies section ---
|
||||
if n_a or n_b:
|
||||
f.write('## Anomalies\n\n')
|
||||
f.write('These are milestone mismatches between an issue in the changelog '
|
||||
'and its referenced PR (or vice-versa). The changelog claim '
|
||||
'"this issue is fixed by this PR, all in this milestone" is '
|
||||
'inconsistent with the actual milestone assignments. '
|
||||
'Resolve by either updating the milestone on the issue/PR or '
|
||||
'removing the misleading entry from the changelog.\n\n')
|
||||
|
||||
if n_a:
|
||||
f.write(f'### Issue in {MILESTONE}, PR in different milestone or no milestone\n\n')
|
||||
by_issue = {}
|
||||
for a in anomalies_a:
|
||||
by_issue.setdefault(a['issue'], []).append(a)
|
||||
for issue_num in sorted(by_issue):
|
||||
entries = by_issue[issue_num]
|
||||
title = entries[0]['issue_title']
|
||||
f.write(f'- {issue_link_title(issue_num, title[:80])}\n')
|
||||
for e in entries:
|
||||
ms_label = fmt_ms(e['pr_milestone'])
|
||||
badge = '🔴' if e['pr_milestone'] is None else '⚠️'
|
||||
f.write(f' - {badge} Referenced {pr_link(e["pr"])} is in milestone **{ms_label}** (expected: {MILESTONE})\n')
|
||||
f.write('\n')
|
||||
|
||||
if n_b:
|
||||
f.write(f'\n### PR in {MILESTONE}, closing issue in different milestone or no milestone\n\n')
|
||||
by_pr = {}
|
||||
for b in anomalies_b:
|
||||
by_pr.setdefault(b['pr'], []).append(b)
|
||||
for pr_num in sorted(by_pr):
|
||||
entries = by_pr[pr_num]
|
||||
title = entries[0]['pr_title']
|
||||
f.write(f'- {pr_link_title(pr_num, title[:80])}\n')
|
||||
for e in entries:
|
||||
ms_label = fmt_ms(e['issue_milestone'])
|
||||
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')
|
||||
|
||||
# --- Context ---
|
||||
f.write('---\n\n')
|
||||
f.write('## Context\n\n')
|
||||
f.write(f'- Milestone: **{MILESTONE}**\n')
|
||||
f.write(f'- Milestone total issues (all states): {len(all_issues)}\n')
|
||||
f.write(f'- Closed issues in milestone: {sum(1 for i in all_issues if i.get("state") == "CLOSED")}\n')
|
||||
f.write(f'- Valid issues after exclusions (after step 5/6a): {len([i for i in all_issues if not issue_excluded(i)])}\n')
|
||||
f.write(f'- Issues referenced in changelog: {len(changelog_issues)}\n')
|
||||
f.write(f'- PRs referenced in changelog: {len(changelog_prs)}\n')
|
||||
|
||||
print(f"Anomaly report written to {OUTPUT}")
|
||||
PYEOF
|
||||
# PRs in milestone but not in changelog
|
||||
missing = sorted(milestone_merged - changelog_prs)
|
||||
print(f'Milestone merged PRs: {len(milestone_merged)}')
|
||||
print(f'Changelog referenced PRs: {len(changelog_prs)}')
|
||||
print(f'PRs in milestone but NOT in changelog: {len(missing)}')
|
||||
for num in missing:
|
||||
pr = next(p for p in milestone_prs if p['number'] == num)
|
||||
print(f' #{num} {pr[\"title\"][:80]}')
|
||||
"
|
||||
```
|
||||
|
||||
This generates `CHANGES-ISSUES.md` containing **only the anomalies** —
|
||||
milestone mismatches between issues and their referenced PRs:
|
||||
For each missing PR found, decide whether it should be added to the
|
||||
changelog or is legitimately excluded (check its labels).
|
||||
|
||||
1. **Issue in milestone, referenced PR in different milestone or no milestone** —
|
||||
the changelog claims a fix here, but the PR is released elsewhere.
|
||||
2. **PR in milestone, closing issue in different milestone or no milestone** —
|
||||
the PR is released here, but the issue it fixes belongs to another version.
|
||||
Also verify that no closed-unmerged PRs remain in the changelog:
|
||||
|
||||
**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).
|
||||
If the report contains a rule violation, the LLM has skipped the pre-flight
|
||||
step and needs to re-run the workflow before re-generating the report.
|
||||
```bash
|
||||
python3 tools/gh.py prs --milestone "<MILESTONE>" --state all | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
closed = [p for p in data if p['state'] == 'CLOSED']
|
||||
if closed:
|
||||
print('WARNING: CLOSED (unmerged) PRs in milestone:')
|
||||
for p in closed:
|
||||
print(f' #{p[\"number\"]} {p[\"title\"][:80]}')
|
||||
"
|
||||
```
|
||||
|
||||
The report is overwritten each time it's generated, reflecting the current
|
||||
state of the milestone and changelog. Every number is rendered as a full
|
||||
`[#N](https://github.com/penpot/penpot/issues/N)` or
|
||||
`[#N](https://github.com/penpot/penpot/pull/N)` link so the report is
|
||||
self-contained and clickable in any Markdown viewer.
|
||||
**Post-edit audit checklist:**
|
||||
- ✅ All referenced PRs are merged (no closed-unmerged artifacts)
|
||||
- ✅ Every merged milestone PR is either in the changelog or excluded by label
|
||||
- ✅ PR and issue counts are internally consistent
|
||||
- ✅ No false-positive PR-to-issue associations
|
||||
|
||||
## Key Principles
|
||||
|
||||
@ -734,7 +449,7 @@ self-contained and clickable in any Markdown viewer.
|
||||
reference if applicable.
|
||||
- **Re-fetch before editing.** Milestones can change — always re-fetch issues
|
||||
before making edits, don't rely on cached data.
|
||||
- **Use `scripts/gh.py`.** Prefer the helper script over raw `gh api` calls for
|
||||
- **Use `tools/gh.py`.** Prefer the helper script over raw `gh api` calls for
|
||||
milestone issue listing and PR detail fetching. It handles GraphQL
|
||||
pagination, batching, and label filtering automatically.
|
||||
- **Verify PR merge status.** Not all closing PRs are merged — community PRs
|
||||
@ -745,20 +460,9 @@ self-contained and clickable in any Markdown viewer.
|
||||
labels. Check both.
|
||||
- **Cross-reference milestone PRs, not just issues.** The `--compare` flag on
|
||||
the `issues` command only compares issue numbers. Merged PRs not linked to
|
||||
any milestone issue can be missed. Use `python3 scripts/gh.py prs --milestone`
|
||||
any milestone issue can be missed. Use `python3 tools/gh.py prs --milestone`
|
||||
for a full PR cross-reference.
|
||||
- **False-positive PR-to-issue associations.** A PR may claim to close an
|
||||
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 (or
|
||||
unassigned). These are anomalies because the changelog pairing is
|
||||
*misleading* — the human needs to decide whether the milestone or the
|
||||
changelog is wrong. All other discrepancies (exclusion labels, missing
|
||||
valid issues, unmerged PR references, duplicates, stale milestone
|
||||
assignments) are **rule violations** that the LLM must fix directly in
|
||||
`CHANGES.md` during step 6a (pre-flight checks). They never appear in
|
||||
the report — if they do, the pre-flight step was skipped.
|
||||
|
||||
@ -39,9 +39,6 @@ Backend RPC command areas without focused memories include access tokens, binfil
|
||||
|
||||
Database migrations live in `backend/src/app/migrations/`; pure SQL migrations are under `backend/src/app/migrations/sql/`. SQL filenames conventionally start with a sequence and verb/table description, e.g. `0026-mod-profile-table-add-is-active-field`. Applied migrations are tracked in the `migrations` table.
|
||||
|
||||
For interactive PostgreSQL access with correct dev defaults, use `scripts/psql`; to dump
|
||||
the current DDL schema, use `scripts/db-schema` (see `mem:scripts/psql`).
|
||||
|
||||
For deeper details on transaction semantics, advisory locks, Transit vs JSON helpers, and dev/test DB URLs: `mem:backend/rpc-db-worker-subtleties`.
|
||||
|
||||
## Background tasks
|
||||
@ -56,14 +53,14 @@ In devenv, backend nREPL is exposed on port 6064.
|
||||
|
||||
### Non-interactive eval (preferred for agents)
|
||||
|
||||
`./scripts/nrepl-eval.mjs` connects to an already-running nREPL server and evaluates code. Session state (defs, `in-ns`) persists across invocations via a stored session ID in `/tmp/penpot-nrepl-session-<host>-<port>`.
|
||||
`./tools/nrepl-eval.mjs` connects to an already-running nREPL server and evaluates code. Session state (defs, `in-ns`) persists across invocations via a stored session ID in `/tmp/penpot-nrepl-session-<host>-<port>`.
|
||||
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs '(+ 1 2)' # single expression
|
||||
./scripts/nrepl-eval.mjs "(require '[my.ns :as ns] :reload)" # reload after edits
|
||||
./scripts/nrepl-eval.mjs -e # inspect last exception (*e)
|
||||
./scripts/nrepl-eval.mjs --reset-session '(def x 0)' # discard session, start fresh
|
||||
./scripts/nrepl-eval.mjs <<'EOF' # multi-expression heredoc
|
||||
./tools/nrepl-eval.mjs '(+ 1 2)' # single expression
|
||||
./tools/nrepl-eval.mjs "(require '[my.ns :as ns] :reload)" # reload after edits
|
||||
./tools/nrepl-eval.mjs -e # inspect last exception (*e)
|
||||
./tools/nrepl-eval.mjs --reset-session '(def x 0)' # discard session, start fresh
|
||||
./tools/nrepl-eval.mjs <<'EOF' # multi-expression heredoc
|
||||
(def x 10)
|
||||
(+ x 20)
|
||||
EOF
|
||||
@ -92,19 +89,13 @@ Fixtures can populate local data for manual testing/perf work. From the backend
|
||||
|
||||
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
|
||||
|
||||
* **Linting:** `pnpm run lint:clj`.
|
||||
* **Formatting:** `pnpm run check-fmt:clj` to check, `pnpm run fmt:clj` to fix. After running `fmt:clj`, `check-fmt:clj` is redundant. Avoid unrelated whitespace diffs.
|
||||
|
||||
**Before linting:** if delimiter errors are suspected (after LLM edits), run
|
||||
`scripts/paren-repair` on the affected files first. Delimiter errors produce
|
||||
misleading linter/compiler output. See `mem:scripts/paren-repair`.
|
||||
* **Linting:** `pnpm run lint` from the repository root.
|
||||
* **Formatting:** `pnpm run check-fmt`. Use `pnpm run fmt` to fix. Avoid unrelated whitespace diffs.
|
||||
|
||||
## Testing
|
||||
|
||||
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory. JVM tests are invoked directly via `clojure -M:dev:test` — there is no pnpm wrapper. If you need to filter output, tee to a temp file first: `clojure -M:dev:test 2>&1 | tee /tmp/penpot-test-output.txt`. See `mem:testing` for execution discipline.
|
||||
IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
|
||||
|
||||
* **Coverage:** If code is added or modified in `src/`, corresponding tests in `test/backend_tests/` must be added or updated.
|
||||
* **Isolated run:** `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace.
|
||||
* **Regression run:** `clojure -M:dev:test` to ensure no regressions in related functional areas.
|
||||
* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
|
||||
|
||||
|
||||
@ -24,12 +24,6 @@ Variant masters are main instances and component roots. Their descendants may th
|
||||
|
||||
Masters are not normally touched through `set-shape-attr`, but touched flags can appear on master shapes through cloning/duplication paths. `add-touched-from-ref-chain` in `app.common.logic.variants` unions touched flags from ancestors into the copy being processed, so upstream/master touched state can affect downstream switch behavior.
|
||||
|
||||
## Swap slots and positional matching
|
||||
|
||||
- A swap slot (stored via `ctk/set-swap-slot`, a `:touched` group `swap-slot-<uuid>`) marks a copy sub-head that was SWAPPED to another component; `compare-children` then pairs it to the main child by slot instead of by `shape-ref`.
|
||||
- Copy sub-heads without a slot are paired to main children by `shape-ref` (seek, not index). `find-near-match` (positional) is only a validator/repair heuristic; validity requires membership of the ref among the near-main parent's children, not index equality (`mem:common/file-change-validation-migration-subtleties`).
|
||||
- Copy child ORDER converges to the main's via the async sync (`moved` branch of `compare-children`); local code must never reorder copy children directly (guards in `:mov-objects`/`:reorder-children`).
|
||||
|
||||
## Cloning paths
|
||||
|
||||
`make-component-instance` in `app.common.types.container` produces a clean component copy through `update-new-shape`, dissociating attrs such as `:touched`, `:variant-id`, and `:variant-name` on cloned shapes.
|
||||
|
||||
@ -48,8 +48,7 @@ Components, variants, and debugging:
|
||||
|
||||
Text and tests:
|
||||
- Shared text data conversion, DraftJS compatibility, modern text content, and derived position data: `mem:common/text-subtleties`.
|
||||
- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/testing`.
|
||||
- Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
|
||||
- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/test-setup`.
|
||||
|
||||
## Areas without focused memories
|
||||
|
||||
|
||||
@ -7,8 +7,6 @@
|
||||
- `set-shape-attr` treats `:position-data` as derived and never touched. Geometry/content-path changes use approximate equality; geometry differences under about 1px can be ignored for touched purposes.
|
||||
- Width/height are excluded from the `is-geometry?` branch in `set-shape-attr`; do not assume all geometry-group attrs follow identical ignore-geometry behavior.
|
||||
- `process-touched-change` marks the owning component modified when a touched shape belongs to a main instance; component-data changes can come from shape ops through this second pass.
|
||||
- Copy structure is guarded at change application: `:mov-objects` (`is-valid-move?`) and `:reorder-children` both refuse to alter children of shapes inside component copies unless the change carries `allow-altering-copies` (sync/swap flows set it). New structural change types must follow the same rule.
|
||||
- `cls/generate-delete-shapes` propagates deletions from INSIDE a component main to the copy shapes referencing them (transitively, all pages of the file) so no dangling `shape-ref`s remain; skipped when the main root itself is deleted (copies then resolve into the deleted component) and for `allow-altering-copies` flows (swap replaces the shape; sync reconciles).
|
||||
|
||||
## Shape tree edits
|
||||
|
||||
@ -21,7 +19,6 @@
|
||||
- Full referential/semantic validation currently runs only when file features contain `"components/v2"`.
|
||||
- Validation starts at root plus orphan shapes, then validates component records. `validate-file!` raises `:validation :referential-integrity` with collected details.
|
||||
- `repair-file` does not mutate data directly; it reduces validation errors into redo changes using `changes-builder`. Callers must apply or persist those changes.
|
||||
- `:missing-slot` fires only for a REAL swap: a copy sub-head whose `shape-ref` is no longer a child of the near main parent. A pure positional mismatch (ref still a sibling elsewhere) is a reorder — valid, realigned by the async component sync; do not "repair" it by assigning swap slots (a slot freezes the child out of normal sync). `fix-missing-swap-slots` (migration 0019) follows the same membership rule.
|
||||
|
||||
## Migrations
|
||||
|
||||
|
||||
@ -8,9 +8,6 @@
|
||||
## Grid assignment
|
||||
|
||||
- Grid `assign-cells` ensures at least one column and row, skips absolute-position children, creates non-tracked rows/cols when children exceed tracked cells, and asserts that assigned cells do not overlap.
|
||||
- `position-absolute?` counts HIDDEN shapes as absolute: hiding a grid child frees its cell on the next `assign-cells`.
|
||||
- `reorder-grid-children` rewrites the parent's `:shapes` to the REVERSE of the sorted cell order, but children with no cell (hidden/absolute) keep their original index — do not "fix" this into moving them to an end; that broke copy/main positional slot alignment (referential-integrity crash).
|
||||
- The `:reorder-children` change it emits is refused on parents inside component copies unless `allow-altering-copies` (same rule as `:mov-objects`); `pcb/reorder-grid-children` also skips copy grids producer-side. Copy child order is owned by the component sync engine.
|
||||
- Grid deassignment removes cells for shapes that are no longer direct children or have become absolute-positioned.
|
||||
- Auto-positioning is not just sorting: some auto cells are converted to manual when empty/manual/span state would break the auto sequence, then auto single-span items can be compacted.
|
||||
- `fix-overlaps` is marked dev-only and removes one overlapping cell, preferring empty cells first. Avoid depending on it as normal production repair.
|
||||
@ -1,29 +1,24 @@
|
||||
# Common Testing and Verification
|
||||
# Common Module Test Setup
|
||||
|
||||
`common/` is CLJC shared code. Tests should cover the relevant runtime(s): JVM for backend/common logic and JS for frontend/exporter behavior. For geometry, component, and file-model changes, JVM tests are common and fast, but JS/browser behavior can differ when WASM modifier math or CLJS-specific state is involved.
|
||||
|
||||
## Unit tests
|
||||
|
||||
READ `mem:testing` FIRST — it defines the execution discipline (no piping, tee to file, preferred commands) that applies to all CLJS/JS and JVM test runs.
|
||||
|
||||
Common tests live under `common/test/common_tests/` and use `clojure.test`.
|
||||
They are CLJC and run on both JVM and JS.
|
||||
## Running tests
|
||||
|
||||
From `common/`:
|
||||
- Full JVM test run: `clojure -M:dev:test`
|
||||
- Full JS test run (always builds, suppressed output): `pnpm run test:quiet`
|
||||
- Full JS test run (always builds, build output visible): `pnpm run test`
|
||||
- Focus a JVM test namespace: `clojure -M:dev:test --focus common-tests.logic.variants-switch-test`
|
||||
- Focus a JVM test var: `clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch`
|
||||
- Focus a JS test namespace: `pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test`
|
||||
- Focus a JS test var: `pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute`
|
||||
- Quiet logging during a JS run: append `--log-level warn` (or `trace|debug|info|warn|error`)
|
||||
- Build JS test target only (no run): `pnpm run build:test`
|
||||
- After `build:test` has been run, run the compiled runner directly: `node target/tests/test.js [--focus ...] [--log-level ...]`.
|
||||
|
||||
New common JS test namespaces must be required/listed in `common_tests/runner.cljc`;
|
||||
new vars in existing namespaces need no runner change. Multiple JVM `--focus` flags
|
||||
compose as a union.
|
||||
```bash
|
||||
pnpm run test:jvm
|
||||
clojure -M:dev:test
|
||||
pnpm run test:jvm --focus common-tests.logic.variants-switch-test
|
||||
clojure -M:dev:test --focus common-tests.logic.variants-switch-test/test-basic-switch
|
||||
pnpm run test:js
|
||||
pnpm run test:quiet
|
||||
pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test
|
||||
pnpm run test:quiet -- --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn
|
||||
pnpm run watch:test
|
||||
```
|
||||
|
||||
Use `test:quiet` for non-interactive JS runs; it buffers `build:test` output and forwards runner args. Common JS runner args support `--focus <namespace-or-var>` and `--log-level trace|debug|info|warn|error`. After `pnpm run build:test`, direct compiled runner focus is faster: `node target/tests/test.js --focus common-tests.logic.comp-sync-test/test-sync-when-changing-attribute --log-level warn`. New common JS test namespaces must be required/listed in `common_tests/runner.cljc`; new vars in existing namespaces need no runner change. Multiple JVM `--focus` flags compose as a union.
|
||||
|
||||
## Test helpers
|
||||
|
||||
@ -6,39 +6,29 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
|
||||
- A section's top-level memory is `<section>/core`. When a section is relevant, read the core memory
|
||||
before focused memories.
|
||||
- Edits/stale refs/duplication cleanup: `mem:memory-maintenance`.
|
||||
- Cross-cutting testing principles, TDD workflow, and anti-patterns: `mem:testing`.
|
||||
|
||||
# Development workflow
|
||||
|
||||
- Commit/PR/issue creation is **on explicit request only**. Before any of these actions, read the relevant memory — don't infer format from prior examples:
|
||||
- Before `git commit` → `mem:workflow/creating-commits` (subject format, body, `AI-assisted-by: model-name` trailer)
|
||||
- Before `gh issue create` → `mem:workflow/creating-issues` (title derivation, body template, labels, Issue Type)
|
||||
- Before `gh pr create` / `gh pr edit` → `mem:workflow/creating-prs` (title format, body structure, "Note:" line)
|
||||
- **Never `git push`, force-push, or modify `git origin`** (or any other remote). The user pushes from their own shell; if a push is required, say so and wait. Never amend a commit that the user has already pushed unless explicitly asked.
|
||||
- Commit only when explicitly asked. Commit/PR format + changelog: `mem:workflow/creating-commits`, `mem:workflow/creating-prs`.
|
||||
- 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.
|
||||
*After making changes, run the applicable lint and format checks for the affected module before considering the work done (per example `mem:backend/core` or `mem:frontend/core`).
|
||||
- Align `let` binding values: when a `let` form has multiple bindings spanning
|
||||
several lines, align the value forms to the same column with spaces.
|
||||
- If you introduce delimiter errors (mismatched parens/brackets) in Clojure/CLJS files,
|
||||
fix them with `scripts/paren-repair` BEFORE running lint/format checks.
|
||||
See `mem:scripts/paren-repair` for usage.
|
||||
- Never run anything that destroys data without explicit permission, including `drop-devenv`, `docker compose down -v`, `docker volume rm ...`. The user's real work lives in the volumes of the shared infra.
|
||||
|
||||
# Project modules
|
||||
|
||||
This is a monorepo. Principles that apply to one module do *not* generally apply to others. Do not make assumptions.
|
||||
|
||||
- `frontend/`: ClojureScript + SCSS SPA/design editor; core conventions: `mem:frontend/core`.
|
||||
- `backend/`: JVM Clojure HTTP/RPC server with PostgreSQL, Redis, storage, mail, and workers; core conventions: `mem:backend/core`. Runtime services and the task-queue vs Pub/Sub topology that constrains horizontal scaling: `mem:prod-infra/core`.
|
||||
- `common/`: shared CLJC data types, geometry, schemas, file/change logic, and utilities; core conventions: `mem:common/core`.
|
||||
- `render-wasm/`: Rust -> WebAssembly Skia renderer consumed by frontend; core conventions: `mem:render-wasm/core`.
|
||||
- `exporter/`: ClojureScript/Node headless Playwright SVG/PDF export; core conventions: `mem:exporter/core`.
|
||||
- `mcp/`: TypeScript Model Context Protocol integration; core conventions: `mem:mcp/core`.
|
||||
- `plugins/`: TypeScript plugin runtime/examples and Plugin API types; core conventions: `mem:plugins/core`.
|
||||
- `library/`: design library workflows; core conventions: `mem:library/core`.
|
||||
- `docs/`: documentation site; core workflow and conventions: `mem:docs/core`.
|
||||
- `frontend/`: ClojureScript + SCSS SPA/design editor.
|
||||
- `backend/`: JVM Clojure HTTP/RPC server with PostgreSQL, Redis, storage, mail, and workers.Runtime services and the task-queue vs Pub/Sub topology that constrains horizontal scaling: `mem:prod-infra/core`.
|
||||
- `common/`: shared CLJC data types, geometry, schemas, file/change logic, and utilities.
|
||||
- `render-wasm/`: Rust -> WebAssembly Skia renderer consumed by frontend.
|
||||
- `exporter/`: ClojureScript/Node headless Playwright SVG/PDF export.
|
||||
- `mcp/`: TypeScript Model Context Protocol integration.
|
||||
- `plugins/`: TypeScript plugin runtime/examples and Plugin API types.
|
||||
- `library/`: design library workflows.
|
||||
- `docs/`: documentation site.
|
||||
|
||||
The memory is structured in a way that you can get the critical information about the
|
||||
module. You can read it from `mem:<MODULE>/core`
|
||||
@ -52,24 +42,6 @@ module. You can read it from `mem:<MODULE>/core`
|
||||
- `experiments/` contains standalone experimental HTML/JS/scripts; treat it as non-core unless the user explicitly asks about it.
|
||||
- `sample_media/` contains sample image/icon media and config used as fixtures/demo material; do not infer app behavior from it.
|
||||
|
||||
# Dev Scripts (scripts/)
|
||||
|
||||
- `scripts/nrepl-eval.mjs` — Evaluate Clojure/ClojureScript code via nREPL.
|
||||
Supports `--backend` (port 6064) and `--frontend` (port 3447) aliases.
|
||||
See `mem:scripts/nrepl-eval`.
|
||||
- `scripts/paren-repair` — Fix mismatched delimiters in Clojure/CLJS files
|
||||
and reformat with cljfmt. Run before lint checks when LLM edits break parens.
|
||||
See `mem:scripts/paren-repair`.
|
||||
- `scripts/psql` — PostgreSQL client wrapper with devenv defaults.
|
||||
Companion: `scripts/db-schema` for DDL dumps. See `mem:scripts/psql`.
|
||||
- `scripts/taiga.py` — Fetch public issues, user stories, and tasks from the
|
||||
Penpot Taiga project without authentication. See `mem:scripts/taiga`.
|
||||
- `scripts/gh.py` — GitHub operations helper: list milestone issues, fetch PR
|
||||
details, compare against CHANGES.md. Requires `gh` CLI. See `mem:scripts/gh`.
|
||||
- `scripts/error-reports.mjs` — Query error reports via RPC API with token
|
||||
authentication. Supports list/get operations with filtering and pagination.
|
||||
See `mem:scripts/error-reports`.
|
||||
|
||||
# Dependency graph
|
||||
|
||||
`frontend -> common`, `backend -> common`, `exporter -> common`, and `frontend -> render-wasm`. Changes in `common` can
|
||||
|
||||
@ -25,7 +25,7 @@ Compose-based dev environment under `docker/devenv/`, driven by `manage.sh`. Par
|
||||
|
||||
## Worker policy
|
||||
|
||||
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. ws0 must be running whenever any ws1+ is running, and is the last instance to stop — `run-devenv --agentic --ws N` (N≥1) auto-starts ws0 first; `stop-devenv` refuses to stop ws0 while any ws1+ is up. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
|
||||
Backend workers run only on ws0. `_env` gates `enable-backend-worker` on `PENPOT_BACKEND_WORKER`; ws1+ inject it as false. ws0 must be running whenever any ws1+ is running, and is the last instance to stop — `run-devenv-agentic --ws N` (N≥1) auto-starts ws0 first; `stop-devenv` refuses to stop ws0 while any ws1+ is up. Workers are pure fire-and-forget: `wrk/submit!` inserts a row into the shared Postgres `task` table and returns; RPC handlers never wait on completion and workers never publish to msgbus. The reason for "ws0 only" is avoiding multi-instance worker races (cron dedup is best-effort across instances, `wrk/submit!` `dedupe` is racy across submitters); details in `mem:prod-infra/core`.
|
||||
|
||||
## Port layout
|
||||
|
||||
@ -44,13 +44,13 @@ Everything else (frontend dev, backend API, exporter, storybook, REPLs, plugin d
|
||||
|
||||
## Tmux + MCP routing
|
||||
|
||||
`docker/devenv/files/start-tmux.sh` is session-level idempotent. Reads `PENPOT_TMUX_ATTACH`. If the session exists it attaches or exits; otherwise creates 4 base windows (frontend watch / storybook / exporter / backend) plus `mcp` (when `enable-mcp` in `PENPOT_FLAGS`) and `serena` (when `SERENA_ENABLED=true`). `run-devenv --agentic` always sets both env vars. The legacy `run-devenv` alias doesn't, hence its 4-window-only session. To switch from a legacy session to agentic, `stop-devenv` then `run-devenv --agentic` — the conditional windows are only added at session create time.
|
||||
`docker/devenv/files/start-tmux.sh` is session-level idempotent. Reads `PENPOT_TMUX_ATTACH`. If the session exists it attaches or exits; otherwise creates 4 base windows (frontend watch / storybook / exporter / backend) plus `mcp` (when `enable-mcp` in `PENPOT_FLAGS`) and `serena` (when `SERENA_ENABLED=true`). `run-devenv-agentic` always sets both env vars. The legacy `run-devenv` alias doesn't, hence its 4-window-only session. To switch from a legacy session to agentic, `stop-devenv` then `run-devenv-agentic` — the conditional windows are only added at session create time.
|
||||
|
||||
MCP plugin routing is same-origin: frontend uses `<public-uri>/mcp/ws`, per-instance nginx proxies to MCP port 4401 in-container. For the plugin↔MCP server wiring (how the browser plugin discovers the URL, the in-memory connection registry, why DB-mediated routing isn't needed), see `mem:mcp/core`.
|
||||
|
||||
## Workspace orchestration (ws1+)
|
||||
|
||||
Workspace directories are user-maintained at `${PENPOT_WORKSPACES_DIR}/wsN`. `run-devenv --agentic --ws i` syncs only when `--sync` is passed, with one exception: if the workspace directory is missing on first use, sync runs implicitly to seed it.
|
||||
Workspace directories are user-maintained at `${PENPOT_WORKSPACES_DIR}/wsN`. `run-devenv-agentic --ws i` syncs only when `--sync` is passed, with one exception: if the workspace directory is missing on first use, sync runs implicitly to seed it.
|
||||
|
||||
`sync-workspace wsN`:
|
||||
1. `assert-clean-git-state` — refuses on `.git/{rebase-apply,rebase-merge,MERGE_HEAD,CHERRY_PICK_HEAD,index.lock}`. No `--sync-force` escape.
|
||||
@ -63,7 +63,7 @@ No `--delete` on the working-tree pass: gitignored caches in the workspace survi
|
||||
|
||||
## CLI surface
|
||||
|
||||
- `run-devenv --agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). Auto-starts ws0 first when the target is ws1+ and ws0 is not yet up.
|
||||
- `run-devenv-agentic [--ws main|0|wsN|N] [--sync] [--serena-context CTX]`: bring one instance up. Agentic only — MCP and Serena windows are always created. Default target main. Errors out if the target is already running. `--sync` is rejected on main; on ws1+ it's optional (forced only when the workspace dir does not exist yet). Auto-starts ws0 first when the target is ws1+ and ws0 is not yet up.
|
||||
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` (N≥1) stops just that workspace. `--ws 0` or no flag stops ws0 + shared infra, refused while any ws1+ is running. `--all` stops every ws highest-first then ws0, then infra.
|
||||
- `run-devenv`: legacy alias, ws0 non-agentic attached.
|
||||
- `attach-devenv [--ws main|0|wsN|N]`: pure attach. Fails fast if instance/session missing.
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
- Source: `exporter/src/`; config: `deps.edn`, `shadow-cljs.edn`, `package.json`; runtime helpers/assets: `vendor/`, `scripts/`.
|
||||
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
|
||||
- Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`.
|
||||
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
||||
|
||||
## HTTP and browser pool
|
||||
|
||||
|
||||
@ -1,152 +0,0 @@
|
||||
# Composable component tests
|
||||
|
||||
A framework concept for systematically testing Penpot's component subsystem
|
||||
(synchronisation/propagation, swaps, variant switches, nesting, overrides), implemented in TWO test
|
||||
suites that share the principles below:
|
||||
|
||||
1. **ClojureScript suite** — in the frontend test tree (`frontend/test/frontend_tests/
|
||||
composable_tests/`), driving a minimally-assembled real app headlessly. The original.
|
||||
2. **TypeScript suite** — a Penpot plugin (`plugins/apps/composable-test-suite/`), driving the FULL
|
||||
production app end-to-end through the Plugin API, with a slightly more elaborate set of
|
||||
abstractions. Runs interactively (panel), remotely (Playwright), and headlessly in CI. Its
|
||||
README is the authoritative operational reference.
|
||||
|
||||
## Shared core idea
|
||||
A test is a **composition of operations** over a starting configuration, plus assertions. You
|
||||
describe a test as data (a setup + a sequence of operations) rather than writing bespoke imperative
|
||||
code, and coverage grows by COMPOSITION: a new variation is one combinator wrapped around existing
|
||||
pieces, not a copied test. Choice points (one-of alternatives, optional steps) EXPAND the
|
||||
composition into a full sweep of variants — one written case stands for a whole matrix of concrete
|
||||
tests.
|
||||
|
||||
## Shared principles
|
||||
- **Every producing object is the accessor interface to what it produces downstream.** An
|
||||
operation — and related objects such as content-creation strategies — is not merely an action:
|
||||
the SAME object instance the case holds is the typed interface through which everything it
|
||||
created or changed is later retrieved, checked, and asserted, parameterized by the situation. A
|
||||
foundation operation exposes accessors for the participants it built; an edit operation exposes
|
||||
its dual check (`assertHasChangedProperty` / `has-property-of`); a choice is recovered by asking
|
||||
the one-of object (`getChoice`/`get-choice`); "did this step run" is asked of the step
|
||||
(`wasApplied`/`applied?`). NEVER reach into a situation (or the document) for something an
|
||||
upstream object produced — ask the producer. This is what keeps sweeps sound (object identity
|
||||
ties the question to the exact node that ran) and what keeps retrieval logic in exactly one
|
||||
place. Particularly explicit in the TS OOP implementation, where these accessors are methods on
|
||||
the operation/strategy classes; repeatedly violating it (reading the document directly,
|
||||
duplicating retrieval) was the most common review correction while building the suites.
|
||||
- **Operations are data with identity.** Each operation node has a unique id at construction and
|
||||
records what it did under that id; interrogation is by identity. Bind an operation to a value
|
||||
ONCE and reuse it in the composition and in every query about it.
|
||||
- **Drive the real production pipeline.** Operations route through genuine Penpot logic — real
|
||||
change functions / real workspace events / the real Plugin API, never raw field writes — so the
|
||||
production watcher's AUTOMATIC propagation is what's under test.
|
||||
- **Roles, not internals.** A starting configuration names its participants (roles). Role→id
|
||||
capture happens when the configuration is built; operation TARGETS resolve at apply-time and may
|
||||
be re-bound, so an operation targeting a role follows it as state-building ops re-point it —
|
||||
which lets a single operation be swept across depth.
|
||||
- **Enumeration is authored, not exhaustive.** Compose only VALID cases, so outcomes are just
|
||||
pass / fail / error — no not-applicable cells.
|
||||
- **Naming discipline.** Penpot domain nouns ("component", "variant") must not name framework
|
||||
abstractions; an operation may name the domain ACTION it performs.
|
||||
- **Operator algebra** (same in both suites): sequence (cartesian product of the steps' variants),
|
||||
one-of (union, choice recorded), optional(X) = one-of([X, skip]), inline assertion ops, trailing
|
||||
asserters.
|
||||
- **Case authoring:** a case carries a CamelCase identifier and a plain-terms description in three
|
||||
parts — situation setup, actions/variations, asserted requirement.
|
||||
|
||||
---
|
||||
|
||||
# ClojureScript suite (frontend test tree)
|
||||
|
||||
Test-only `.cljs` code in the frontend test tree (nothing "common" about it). A **situation** =
|
||||
the in-memory file value + named roles + `:vars` + an ordered applied-log. Operations are records
|
||||
implementing `IOperation`/`apply-to` (`apply` collides with core). Assertions = inline `Test` ops
|
||||
and/or a trailing asserter; the runner makes no judgment. Failures carry `describe-applied` (the
|
||||
transcript), which is what makes a failing variant in a sweep identifiable.
|
||||
|
||||
Layout: `core.cljs` (the domain-agnostic engine: situation, identity/transcript, roles/targets,
|
||||
operators, runners), `comp/setups.cljs` (setups + role accessors), `comp/nodes.cljs` (the component
|
||||
operations and their check duals), `interpreter.cljs` (runs cases against the real frontend),
|
||||
`comp/sync_test.cljs` (the cases; registered in `frontend_tests/runner.cljs`). Case letters B..N;
|
||||
the sweeps (K: depth × edit-precedence; L: swaps; M: variant switches; N: rotated-instance
|
||||
geometry, on the #10109 fix branch until merged) are the flagship pattern — read them before
|
||||
writing a new sweep.
|
||||
|
||||
**Scenario lineage model** (behind the sweeps): scenario ops track named component lineages as
|
||||
objects under `:vars`, each holding the FIXED deepest origin (`:remote-*`), the ADVANCING outer
|
||||
main (`:main-*`), and per-nesting-level data whose `:nested-head` (the deepest instance at that
|
||||
level, found by descending the `:shape-ref` chain — matching chain MEMBERSHIP, not terminus) is
|
||||
the swap/switch target, anchored by its swap-stable parent. Nesting seeks the FIXED origin, not
|
||||
the advancing main — that is what makes each level's `:nested-head` land on the deepest instance.
|
||||
A variant nesting re-points the lineage's remote to the chosen member. Construction lesson:
|
||||
cross-level propagation requires progressively NESTED levels (one variant + plain wraps); sibling
|
||||
nestings do not propagate between each other.
|
||||
|
||||
**Interpreter:** installs the situation's files into the global `st/state` (aux files tagged
|
||||
`:library-of`), starts the real `watch-component-changes` (+ harness `watch-undo-stack`), maps
|
||||
event-ops to REAL workspace events (`dwsh/update-shapes`, `dwl/component-swap`,
|
||||
`dwv/variants-switch`, `dwt/increase-rotation` — which runs the `check-delta` placement
|
||||
classification — `dwt/update-dimensions`, `dwu/undo`, `dwl/sync-file`, …) and runs sync-ops'
|
||||
`apply-to` against the live store file; awaits settlement (idle-gap heuristic + per-op grace) and
|
||||
re-reads `:file` each step so the shared accessors keep working.
|
||||
STORE-SWAP IMMUNITY: other test namespaces `set!` `st/state`/`st/stream` and never restore, while
|
||||
the `app.main.refs` lenses stay bound to the ORIGINAL atoms — propagation then dies silently. The
|
||||
interpreter captures the atoms at namespace-load time and re-`set!`s them per variant.
|
||||
|
||||
Running: `cd frontend && pnpm run build:test`, then
|
||||
`node target/tests/test.js --focus frontend-tests.composable-tests.comp.sync-test`
|
||||
(var-level focus for one case).
|
||||
|
||||
**Fidelity warning:** the harness drives a MINIMALLY-ASSEMBLED app — only some
|
||||
`initialize-workspace` subscriptions are wired. Risk = SILENT UNDER-WIRING (e.g. undo needs the
|
||||
harness `watch-undo-stack`). When a case needs app behaviour beyond a raw edit, check for an
|
||||
unwired subscription and verify by PROBING store state, not by trusting a green assertion.
|
||||
|
||||
**Caveats:** inline `Test` exceptions are UNCAUGHT on the frontend (crash the runner — assert in
|
||||
the trailing asserter). `(optional (in-sequence …))` is not flattened for the interpreter — use
|
||||
independent optionals. The Serena/clj-kondo cache for `nodes.cljs` goes stale (phantom symbols) —
|
||||
trust the build. Cross-namespace global-state leaks land in this suite first; suspect them before
|
||||
the framework on inexplicable full-run-only failures. Case H's `sync-file` schedules a delayed RPC
|
||||
that fails headless (benign; absorbed by per-op grace).
|
||||
|
||||
---
|
||||
|
||||
# TypeScript suite (the plugin) — full e2e
|
||||
|
||||
`plugins/apps/composable-test-suite/` — same principles against the FULL production app through the
|
||||
Plugin API (real frontend, real propagation). Continuation of the CLJS suite per issue #10584.
|
||||
Operational details (build/run, connect URL, remote control, reading logs, auto-reload, CI): the
|
||||
plugin README.
|
||||
|
||||
Distinguishing abstractions (the OOP articulation of the shared principles):
|
||||
- `TestCase {identifier, description, operation}` with the three-part description mandated in the
|
||||
constructor docstring.
|
||||
- The accessor-interface principle is class-level: foundation operations (e.g.
|
||||
`OpCreateSimpleComponentWithCopy`) expose the roles they build; **content-creation strategies**
|
||||
(pluggable: what content a foundation builds around) expose accessors for the content they
|
||||
created; edit operations expose their checks (`OpChangeProperty.assertHasChangedProperty`);
|
||||
`OpOneOf`/`OpOptional` are queried for what ran. Tests never grope the document for something a
|
||||
producer can be asked for.
|
||||
- `ShapeProp` model: property duals with numeric tolerance; rotation is a writable attr, height
|
||||
goes via resize (readonly in the Plugin API).
|
||||
- `TestSuite` enumerates cases into a `TestTree` with stable per-test ids;
|
||||
`run(ids, TestRunObserver)` is the ONLY output channel — the framework is UI-free by
|
||||
construction. `plugin.ts` (panel adapter), `main.ts` (panel UI) and `src/ci/headless.ts`
|
||||
(CI adapter) are three thin consumers.
|
||||
- Cases live in `src/composable-tests/cases/` as `case<Identifier>.ts` (e.g. `MainEditSyncs` — the
|
||||
sweep that found #10109).
|
||||
- Panel checkboxes carry stable DOM ids (case identifier / `Identifier-N` composites) for remote
|
||||
control via Playwright; recipe in the README.
|
||||
|
||||
## CI
|
||||
Headless per-PR gate: `.github/workflows/tests-composable-suite.yml` runs
|
||||
`pnpm --filter composable-test-suite run test:ci` — mocked backend (frontend e2e static server +
|
||||
Playwright RPC fixtures, no backend/login), the in-sandbox bundle injected via `ɵloadPlugin`,
|
||||
results streamed via console markers, `TEST_FILTER` by identifier substring. The mocked backend is
|
||||
NOT a limitation for this suite (everything asserted is frontend store logic; empirically
|
||||
confirmed against the interactive runs). Architecture mirrors `plugin-api-test-suite`'s CI driver;
|
||||
the mock harness exists in THREE places that must stay in sync (provenance note in `ci/run-ci.ts`).
|
||||
Details: README, "Running in CI".
|
||||
|
||||
## Substrate
|
||||
`mem:common/test-setup`, `mem:common/component-data-model`, `mem:common/component-swap-pipeline`,
|
||||
`mem:frontend/testing`.
|
||||
@ -23,14 +23,9 @@ From `frontend/`:
|
||||
- JS lint currently no-ops via `pnpm run lint:js`.
|
||||
- SCSS lint: `pnpm run lint:scss`.
|
||||
- Format checks: `pnpm run check-fmt:clj`, `pnpm run check-fmt:js`, `pnpm run check-fmt:scss`.
|
||||
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`. After running `fmt:*`, `check-fmt:*` is redundant.
|
||||
- Format fix: `pnpm run fmt`, or targeted `fmt:clj` / `fmt:js` / `fmt:scss`.
|
||||
- Translation formatting after i18n edits: `pnpm run translations`.
|
||||
|
||||
**Before linting:** if delimiter errors are suspected (after LLM edits, or
|
||||
lint/compiler reports syntax errors), run `scripts/paren-repair` on the
|
||||
affected files first. Delimiter errors produce misleading linter output.
|
||||
See `mem:scripts/paren-repair`.
|
||||
|
||||
## Focused memory routing
|
||||
|
||||
UI and packages:
|
||||
@ -52,7 +47,6 @@ Diagnostics and validation:
|
||||
- Source-edit compile/hot-reload diagnostics: `mem:frontend/compile-diagnostics`.
|
||||
- Runtime crash recovery: `mem:frontend/handling-crashes`.
|
||||
- Tests and live verification: `mem:frontend/testing`.
|
||||
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
||||
- Real pointer/keyboard gesture reproduction: `mem:frontend/playwright-gestures`.
|
||||
|
||||
## Areas without focused memories
|
||||
|
||||
@ -10,12 +10,6 @@ You have access to two tools for finding errors in Clojure source code (which yo
|
||||
The latter is needed because syntax errors in parentheses give an uninformative compiler error, and the second
|
||||
tool can often find the exact location of such errors.
|
||||
|
||||
When delimiter errors are detected (typically from lint or compiler output),
|
||||
fix the affected files with `scripts/paren-repair`. The `clj_check_parentheses`
|
||||
MCP tool can also pinpoint the error location when available, but it is not
|
||||
required — standard build errors are usually enough.
|
||||
See `mem:scripts/paren-repair`.
|
||||
|
||||
## Runtime patching with `set!`
|
||||
|
||||
Some frontend vars are deliberately mutable escape hatches for runtime instrumentation or circular-dependency patching.
|
||||
|
||||
@ -4,18 +4,15 @@ Frontend validation: CLJS + React/Rumext + RxJS/Potok; SCSS modules; shared CLJC
|
||||
|
||||
## Unit tests
|
||||
|
||||
READ `mem:testing` FIRST — it defines the execution discipline (no piping, tee to file, preferred commands) that applies to all CLJS/JS test runs.
|
||||
|
||||
Frontend unit tests live under `frontend/test/frontend_tests/` and use `cljs.test`. They should be deterministic, avoid DOM/UI integration where possible, and mock side effects such as RPC, storage, timers, or network access.
|
||||
|
||||
From `frontend/`:
|
||||
- Full unit test run (always builds, suppressed output): `pnpm run test:quiet`.
|
||||
- Full unit test run (always builds, build output visible): `pnpm run test`.
|
||||
- Full unit test run: `pnpm run test:quiet`.
|
||||
- Focus a frontend CLJS test namespace: `pnpm run test:quiet -- --focus frontend-tests.logic.components-and-tokens`.
|
||||
- Focus one frontend CLJS test var: `pnpm run test:quiet -- --focus frontend-tests.logic.components-and-tokens/change-spacing-token-in-main-updates-copy-layout`.
|
||||
- Quiet `app.*` logging during a run: append `--log-level warn` (or `trace|debug|info|warn|error`).
|
||||
- Build test target only (no run): `pnpm run build:test`.
|
||||
- After `build:test` has been run, run the compiled runner directly: `node target/tests/test.js [--focus ...] [--log-level ...]`.
|
||||
- Build test target only: `pnpm run build:test`.
|
||||
- After `pnpm run build:test`, direct compiled runner focus is faster: `node target/tests/test.js --focus frontend-tests.logic.components-and-tokens/change-spacing-token-in-main-updates-copy-layout`.
|
||||
- Watch tests: `pnpm run watch:test`.
|
||||
|
||||
New frontend test namespaces must be required/listed in `frontend_tests/runner.cljs`; new vars in existing namespaces need no runner change.
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
- Source: `library/src/`; tests: `library/test/`; experimentation/docs: `playground/`, `docs/`; config: `shadow-cljs.edn`, `deps.edn`, `package.json`.
|
||||
- From `library/`: build `pnpm run build`; bundle helper `pnpm run build:bundle` or `./scripts/build`; tests `pnpm run test`; watch `pnpm run watch` / `pnpm run watch:test`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
|
||||
- When changing file-format construction or export behavior in `common/`, consider whether `@penpot/library` should be tested because it constructs Penpot files outside the app UI.
|
||||
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
||||
|
||||
## JS API and builder state
|
||||
|
||||
|
||||
@ -85,7 +85,6 @@ From the `mcp/` directory, run
|
||||
|
||||
* `pnpm run build` to test the build of all packages
|
||||
* `pnpm run fmt` to apply the auto-formatter
|
||||
* Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
||||
|
||||
## Devenv plugin/server wiring
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@
|
||||
|
||||
- From `plugins/`: install `pnpm -r install`; runtime dev server `pnpm run start` or `pnpm run start:app:runtime`; sample plugin `pnpm run start:plugin:<name>`; build runtime `pnpm run build:runtime`; build plugins `pnpm run build:plugins`; lint `pnpm run lint`; format `pnpm run format:check` / `pnpm run format`; tests `pnpm run test`; e2e `pnpm run test:e2e`.
|
||||
- If a change affects public Plugin API types or runtime, update `plugins/CHANGELOG.md`. Prefix type/signature entries with `**plugin-types:**`; runtime behavior entries with `**plugin-runtime:**`.
|
||||
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
||||
- JS Plugin API behavior inside Penpot app: `mem:frontend/plugin-api-to-cljs-binding`; TS declarations are not runtime code; many API objects are CLJS proxies in `frontend/src/app/plugins/*.cljs`.
|
||||
|
||||
## Sandbox and global cleanup
|
||||
|
||||
@ -26,7 +26,6 @@ From `render-wasm/`:
|
||||
- Build/copy frontend artifacts: `./build`.
|
||||
- Watch rebuild: `./watch`.
|
||||
- Rust tests: `./test` or `cargo test <name>`.
|
||||
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
||||
- Lint: `./lint`.
|
||||
- Format check: `cargo fmt --check`.
|
||||
|
||||
|
||||
@ -1,289 +0,0 @@
|
||||
# Error Reports CLI Tool
|
||||
|
||||
`scripts/error-reports.mjs` is a Node.js CLI tool for querying Penpot error reports via the RPC API. Provides access to error logs with filtering, pagination, and multiple output formats.
|
||||
|
||||
## When to use
|
||||
|
||||
- Querying error reports from the database for debugging or analysis
|
||||
- Filtering errors by source, kind, tenant, or backend version
|
||||
- Exporting error data in JSON, NDJSON, or table format
|
||||
- Computing error statistics (top signatures, version, source, audit-log kind, hourly distribution, bursts, heatmap)
|
||||
- Investigating specific error reports by ID
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js with `commander` and `dotenv` packages installed (in root `package.json`)
|
||||
- Running Penpot backend with error-reports RPC endpoints
|
||||
- Access token with `error-reports:read` permission
|
||||
|
||||
## Configuration
|
||||
|
||||
Create a `.env` file in the project root:
|
||||
|
||||
```bash
|
||||
PENPOT_API_URI=http://localhost:3450
|
||||
PENPOT_ACCESS_TOKEN=<your-token>
|
||||
```
|
||||
|
||||
Grant the required permission to your access token:
|
||||
|
||||
```sql
|
||||
UPDATE access_token
|
||||
SET perms = ARRAY['error-reports:read']::text[],
|
||||
updated_at = now()
|
||||
WHERE id = '<token-uuid>';
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
./scripts/error-reports.mjs <command> [options]
|
||||
```
|
||||
|
||||
### Commands
|
||||
|
||||
#### `list` - List error reports with pagination and filters
|
||||
|
||||
```bash
|
||||
./scripts/error-reports.mjs list [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-l, --limit <n>` | Max items per page (max: 200) | `50` |
|
||||
| `--from <date>` | ISO timestamp — oldest boundary (items after this) | — |
|
||||
| `--to <date>` | ISO timestamp — newest boundary (items before this) | — |
|
||||
| `--since <date>` | ISO timestamp — explicit cursor for manual pagination | — |
|
||||
| `--since-id <uuid>` | Fetch errors after this ID (cursor pagination) | — |
|
||||
| `-s, --source <name>` | Filter by source (see source names below) | — |
|
||||
| `-p, --profile-id <uuid>` | Filter by profile ID | — |
|
||||
| `-k, --kind <kind>` | Filter by kind (string) | — |
|
||||
| `-t, --tenant <tenant>` | Filter by tenant (string) | — |
|
||||
| `--version <version>` | Filter by version | — |
|
||||
| `--hint <text>` | Filter by hint (ILIKE match) | — |
|
||||
| `-a, --all` | Fetch all pages automatically (streams output) | `false` |
|
||||
| `-f, --format <type>` | Output format: `json`, `table`, or `ndjson` | `table` |
|
||||
| `--normalize-hints` | Normalize hints by stripping dynamic values | `false` |
|
||||
| `-o, --output <file>` | Write output to file instead of stdout | — |
|
||||
| `--env <path>` | Custom .env file path | `.env` |
|
||||
| `-h, --help` | Show help message | — |
|
||||
|
||||
**Streaming behavior:** With `--all`, output must be `ndjson` or `table`; `--all --format json` is rejected because `--all` streams output. `--all --format table` prints rows immediately. `--format ndjson` always streams one JSON object per line.
|
||||
|
||||
#### `get` - Get a single error report by ID
|
||||
|
||||
```bash
|
||||
./scripts/error-reports.mjs get [options]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description | Required |
|
||||
|------|-------------|----------|
|
||||
| `--id <uuid>` | Error report ID | Yes (or --error-id) |
|
||||
| `--error-id <id>` | Error report error-id | Yes (or --id) |
|
||||
| `-f, --format <type>` | Output format: `json` or `table` | No (default: `table`) |
|
||||
| `--env <path>` | Custom .env file path | No (default: `.env`) |
|
||||
| `-h, --help` | Show help message | No |
|
||||
|
||||
#### `stats` - Compute error report statistics
|
||||
|
||||
```bash
|
||||
./scripts/error-reports.mjs stats [options]
|
||||
```
|
||||
|
||||
Reads from `--input <file>`, stdin (piped), or fetches from API. Computes aggregations by signature, version, source, audit-log kind, hour, optional 5-minute bursts, and optional day-of-week × hour heatmap.
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--from <date>` | Start of interval (ISO timestamp) | — |
|
||||
| `--to <date>` | End of interval (ISO timestamp) | — |
|
||||
| `--limit <n>` | Items per page when fetching from API | `200` |
|
||||
| `--input <file>` | Read from local JSON/NDJSON file instead of API | — |
|
||||
| `--burst` | Detect 5-minute windows above 3× the average rate | `false` |
|
||||
| `--heatmap` | Show day-of-week × hour-of-day heatmap | `false` |
|
||||
| `-f, --format <type>` | Output format: `json` or `table` | `table` |
|
||||
| `--env <path>` | Custom .env file path | `.env` |
|
||||
|
||||
## Source Names
|
||||
|
||||
The `--source` filter accepts these values:
|
||||
|
||||
- `logging`
|
||||
- `audit-log`
|
||||
- `rlimit`
|
||||
|
||||
## Hint Normalization
|
||||
|
||||
With `--normalize-hints` (or always in `stats`), hints are normalized by stripping dynamic values:
|
||||
|
||||
1. File IDs in file-id context → `<file-id>`
|
||||
2. UUIDs (8-4-4-4-12 hex) → `<uuid>`
|
||||
3. Numeric IDs in parentheses `(12345)` → `(<id>)`
|
||||
4. Elapsed times (`7.5s`, `2m3.027s`) → `<elapsed>`
|
||||
5. URIs (`https://...`) → `<uri>`
|
||||
6. Unicode quotes and whitespace normalized
|
||||
|
||||
## Examples
|
||||
|
||||
### List recent errors
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --limit 10
|
||||
```
|
||||
|
||||
### Time-range query (today)
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --from 2026-07-23T00:00:00Z --to 2026-07-23T23:59:59Z --all
|
||||
```
|
||||
|
||||
### Stream all errors as NDJSON
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --all --format ndjson > errors.ndjson
|
||||
```
|
||||
|
||||
### Save to file with --output
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --all --format ndjson -o errors.ndjson
|
||||
./scripts/error-reports.mjs list --format json -o errors.json
|
||||
```
|
||||
|
||||
### Filter by source
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --source audit-log --limit 20
|
||||
```
|
||||
|
||||
### Filter by kind
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --kind exception-page
|
||||
```
|
||||
|
||||
### Filter by tenant
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --tenant production
|
||||
```
|
||||
|
||||
### Filter by version
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --version 2.1.0
|
||||
```
|
||||
|
||||
### Search by hint (partial match)
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --hint "NullPointerException"
|
||||
```
|
||||
|
||||
### Fetch all errors with pagination
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --all
|
||||
```
|
||||
|
||||
### Get specific error by ID
|
||||
```bash
|
||||
./scripts/error-reports.mjs get --id 550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
### Output as JSON
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --limit 5 --format json
|
||||
```
|
||||
|
||||
### Combine filters
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --source audit-log --kind exception-page --tenant production --limit 50
|
||||
```
|
||||
|
||||
### Stats with burst and heatmap analysis
|
||||
```bash
|
||||
./scripts/error-reports.mjs stats --from 2026-07-23T00:00:00Z --to 2026-07-23T23:59:59Z --burst --heatmap
|
||||
```
|
||||
|
||||
### Stats from file
|
||||
```bash
|
||||
./scripts/error-reports.mjs stats --input errors.json
|
||||
```
|
||||
|
||||
### Stats from pipe
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --all --format json | ./scripts/error-reports.mjs stats
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
### Table (default)
|
||||
Human-readable table format for terminal display. With `--all`, rows stream as they arrive.
|
||||
|
||||
### JSON
|
||||
Single page: `{items: [...], nextSince, nextId}`. `--all` cannot be combined with `--format json`; use `--format ndjson` for streaming.
|
||||
|
||||
### NDJSON
|
||||
One JSON object per line, always streaming. Pipe-friendly: `| jq -c '.hint'`, `| wc -l`.
|
||||
|
||||
## Pagination
|
||||
|
||||
The server returns items in **ascending** order (oldest first). Cursor pagination uses `--since` / `--since-id` to fetch the next page of newer items.
|
||||
|
||||
### Manual pagination
|
||||
Use `--since` and `--since-id` with values from `nextSince` and `nextId` in the response:
|
||||
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --limit 50
|
||||
# Use nextSince and nextId from response
|
||||
./scripts/error-reports.mjs list --limit 50 --since "2026-01-20T10:29:00Z" --since-id "next-uuid"
|
||||
```
|
||||
|
||||
### Automatic pagination
|
||||
Use `--all` to fetch all pages automatically (streams output):
|
||||
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --all
|
||||
```
|
||||
|
||||
### Time-range queries
|
||||
Use `--from` and `--to` to bound the query. These map to the server's `--since` and `--until` parameters:
|
||||
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --from 2026-07-20T00:00:00Z --to 2026-07-23T23:59:59Z --all
|
||||
```
|
||||
|
||||
## Key principles
|
||||
|
||||
- **Authentication required** - Uses access token with `error-reports:read` permission
|
||||
- **API endpoint configurable** - Set via `PENPOT_API_URI` in `.env` file
|
||||
- **Table is default format** - Use `--format json` for structured JSON, `--format ndjson` for streaming
|
||||
- **Streaming with --all** - Items print as they arrive, no buffering. Use `--format ndjson` or `--format table`; `--all --format json` is rejected.
|
||||
- **Filters are combinable** - All filter options can be used together
|
||||
- **Both flag formats supported** - `--option=value` and `--option value` both work
|
||||
- **Ascending order** - Server returns oldest items first (changed from DESC)
|
||||
|
||||
## Error handling
|
||||
|
||||
The tool provides helpful error messages for common issues:
|
||||
|
||||
- **Missing configuration**: Shows setup instructions for `.env` file
|
||||
- **Authentication errors (401)**: Indicates invalid or expired token
|
||||
- **Authorization errors (403)**: Indicates missing `error-reports:read` permission
|
||||
- **RPC errors**: Displays error code and message from the API
|
||||
|
||||
## Integration with other scripts
|
||||
|
||||
- **jq**: Pipe NDJSON output to `jq` for further processing
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --all --format ndjson | jq -c '{id, hint}'
|
||||
```
|
||||
- **stats from pipe**: Fetch data once, compute stats
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --all --format ndjson | ./scripts/error-reports.mjs stats
|
||||
```
|
||||
- **stats from NDJSON pipe**: Works with NDJSON format too
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --all --format ndjson | ./scripts/error-reports.mjs stats
|
||||
```
|
||||
- **grep/search**: Filter output by specific patterns
|
||||
- **--output**: Save to file without shell redirection
|
||||
```bash
|
||||
./scripts/error-reports.mjs list --all --format ndjson -o errors.ndjson
|
||||
```
|
||||
@ -1,80 +0,0 @@
|
||||
# GitHub operations helper
|
||||
|
||||
`scripts/gh.py` is a multi-purpose CLI for querying the penpot/penpot GitHub
|
||||
repository via GraphQL and REST APIs through the authenticated `gh` CLI.
|
||||
|
||||
## When to use
|
||||
|
||||
- Listing issues in a milestone (for changelog generation).
|
||||
- Finding issues with no milestone.
|
||||
- Fetching PR details by number or by milestone.
|
||||
- Comparing milestone issues against CHANGES.md to find missing entries.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `gh` CLI authenticated (`gh auth status`).
|
||||
- Python 3.8+.
|
||||
|
||||
## Subcommands
|
||||
|
||||
### `issues`
|
||||
|
||||
List issues in a milestone, with filtering by state, labels, and project status.
|
||||
|
||||
```bash
|
||||
# Closed issues in a milestone (default)
|
||||
python3 scripts/gh.py issues "2.16.0"
|
||||
|
||||
# All issues in a milestone
|
||||
python3 scripts/gh.py issues "2.16.0" --state all
|
||||
|
||||
# Issues with no milestone
|
||||
python3 scripts/gh.py issues none
|
||||
python3 scripts/gh.py issues none --state open
|
||||
|
||||
# Filter by label (include only)
|
||||
python3 scripts/gh.py issues "2.16.0" --label "bug"
|
||||
python3 scripts/gh.py issues "2.16.0" --label "bug,regression"
|
||||
|
||||
# Exclude by label
|
||||
python3 scripts/gh.py issues "2.16.0" --exclude "release blocker,no changelog"
|
||||
|
||||
# Show only issues NOT yet in CHANGES.md
|
||||
python3 scripts/gh.py issues "2.16.0" --compare CHANGES.md
|
||||
```
|
||||
|
||||
**Default filters** (override with flags):
|
||||
- Issues with type "Task" are excluded (`--include-tasks` to keep them).
|
||||
- Issues with "Rejected" project status are excluded (`--include-rejected` to keep them).
|
||||
|
||||
**Output**: JSON array to stdout; progress to stderr.
|
||||
|
||||
### `prs`
|
||||
|
||||
Fetch PR details by number or by milestone.
|
||||
|
||||
```bash
|
||||
# Fetch specific PRs
|
||||
python3 scripts/gh.py prs 9179 9204 9311
|
||||
|
||||
# Read PR numbers from file
|
||||
python3 scripts/gh.py prs --file prs.txt
|
||||
|
||||
# Read PR numbers from stdin
|
||||
cat prs.txt | python3 scripts/gh.py prs --stdin
|
||||
|
||||
# All PRs in a milestone (default: merged only)
|
||||
python3 scripts/gh.py prs --milestone "2.16.0"
|
||||
|
||||
# All PRs in a milestone (all states)
|
||||
python3 scripts/gh.py prs --milestone "2.16.0" --state all
|
||||
```
|
||||
|
||||
**Output**: JSON array to stdout; progress to stderr.
|
||||
|
||||
## Key principles
|
||||
|
||||
- All output is JSON — pipe into `jq` or other tools for further processing.
|
||||
- Milestone lookup is by exact title match.
|
||||
- `issues` subcommand auto-paginates (100 items per page).
|
||||
- `prs` subcommand batches PR number lookups (50 per GraphQL query).
|
||||
@ -1,148 +0,0 @@
|
||||
# nREPL Eval
|
||||
|
||||
Evaluate Clojure (or ClojureScript) code via a running nREPL server using
|
||||
`scripts/nrepl-eval.mjs` — a standalone CLI application.
|
||||
|
||||
Session state (defs, in-ns, etc.) persists across invocations via a stored
|
||||
session ID, so you can build up state incrementally.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
node scripts/nrepl-eval.mjs [options] [<code>]
|
||||
# or
|
||||
./scripts/nrepl-eval.mjs [options] [<code>]
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `--backend` | Connect to backend nREPL (port 6064) | — |
|
||||
| `--frontend` | Connect to frontend nREPL (port 3447) | — |
|
||||
| `-p, --port PORT` | nREPL server port | `6064` |
|
||||
| `-H, --host HOST` | nREPL server host | `127.0.0.1` |
|
||||
| `-t, --timeout MS` | Timeout in milliseconds | `120000` |
|
||||
| `--reset-session` | Discard stored session and start fresh | — |
|
||||
| `-e, --last-error` | Evaluate `*e` to retrieve the last exception | — |
|
||||
| `-h, --help` | Show help message | — |
|
||||
|
||||
- `--backend` and `--frontend` are mutually exclusive.
|
||||
- Explicit `--port` is overridden when `--backend`/`--frontend` is used.
|
||||
|
||||
## When to Use
|
||||
|
||||
1. **Evaluate Clojure code** during development — test functions, inspect
|
||||
state, or run experiments against a running Clojure process.
|
||||
2. **Verify that edited files compile** — require namespaces with `:reload`
|
||||
to pick up changes.
|
||||
3. **Inspect the last exception** after a failed evaluation — use `-e` to
|
||||
print the error stored in `*e`.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Session management
|
||||
|
||||
Sessions are persisted to `/tmp/penpot-nrepl-session-<host>-<port>`. State
|
||||
carries across calls automatically:
|
||||
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs '(def x 42)'
|
||||
./scripts/nrepl-eval.mjs 'x'
|
||||
# => 42
|
||||
```
|
||||
|
||||
Reset the session to start fresh:
|
||||
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs --reset-session '(def x 0)'
|
||||
```
|
||||
|
||||
### Evaluate code
|
||||
|
||||
**Single expression (inline) — uses default port 6064:**
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs '(+ 1 2 3)'
|
||||
```
|
||||
|
||||
**Backend nREPL (explicit):**
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs --backend '(+ 1 2 3)'
|
||||
```
|
||||
|
||||
**Frontend nREPL:**
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs --frontend '(js/alert "hi")'
|
||||
```
|
||||
|
||||
**Multiple expressions via heredoc (recommended — avoids escaping issues):**
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs <<'EOF'
|
||||
(def x 10)
|
||||
(+ x 20)
|
||||
EOF
|
||||
```
|
||||
|
||||
**Override with a different port:**
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs -p 7888 '(+ 1 2 3)'
|
||||
```
|
||||
|
||||
### Inspect last exception
|
||||
|
||||
After code throws an error, retrieve the full exception details:
|
||||
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs -e
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Require a namespace with reload:**
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs "(require '[my.namespace :as ns] :reload)"
|
||||
```
|
||||
|
||||
**Test a function:**
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs "(ns/my-function arg1 arg2)"
|
||||
```
|
||||
|
||||
**Long-running operation with custom timeout:**
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs -t 300000 "(long-running-fn)"
|
||||
```
|
||||
|
||||
### Accessing Private Functions
|
||||
|
||||
Private functions (declared with `^:private` or `defn-`) cannot be called
|
||||
directly from outside their namespace. Use the var quote syntax `#'` to
|
||||
access the underlying var:
|
||||
|
||||
**This fails:**
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs "(app.rpc.commands.error-reports/build-list-query {})"
|
||||
# => Syntax error: app.rpc.commands.error-reports/build-list-query is not public
|
||||
```
|
||||
|
||||
**This works:**
|
||||
```bash
|
||||
./scripts/nrepl-eval.mjs "(#'app.rpc.commands.error-reports/build-list-query {})"
|
||||
# => Returns the result
|
||||
```
|
||||
|
||||
The `#'` reader macro resolves to `(var ...)`, giving you direct access to
|
||||
the var regardless of its visibility modifier. The syntax is `#'` followed
|
||||
by the fully qualified symbol.
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Default port is 6064** — just pass code directly, no `-p` needed when
|
||||
your nREPL server is on 6064. Use `--backend` (6064) or `--frontend` (3447)
|
||||
as quick aliases. Use `-p <PORT>` for any other port.
|
||||
- **Always use `:reload`** when requiring namespaces to pick up file changes.
|
||||
- **Session is reused** across invocations — defs, in-ns, and var bindings
|
||||
persist. Use `--reset-session` to clear.
|
||||
- **Do not start any server** — the tool connects to an existing nREPL
|
||||
server, it is not the agent's responsibility to start the nREPL server
|
||||
(assume the server is already running on the specified port).
|
||||
@ -1,43 +0,0 @@
|
||||
# Paren-Repair
|
||||
|
||||
`scripts/paren-repair` fixes mismatched parentheses, brackets, and braces in
|
||||
Clojure/ClojureScript files, then reformats them with cljfmt.
|
||||
|
||||
## When to use
|
||||
|
||||
- After LLM edits introduce broken delimiters — proactively run it on files
|
||||
you just touched.
|
||||
- When lint (clj-kondo), the Clojure compiler, or shadow-cljs report syntax
|
||||
errors mentioning mismatched/unclosed delimiters, reader errors, or
|
||||
unexpected EOF.
|
||||
- Before running lint/format checks — delimiter errors make linter output
|
||||
misleading. Fix them first, then lint.
|
||||
|
||||
## How to use (CLI)
|
||||
|
||||
```bash
|
||||
# File mode (in-place fix + format)
|
||||
bb scripts/paren-repair path/to/file.clj
|
||||
|
||||
# Pipe mode (stdin → fixed code to stdout)
|
||||
echo '(def x 1' | bb scripts/paren-repair
|
||||
|
||||
# Help
|
||||
bb scripts/paren-repair --help
|
||||
```
|
||||
`bb` must be invoked from the repo root so the path `scripts/paren-repair` resolves.
|
||||
|
||||
## Native Tool Available (opencode)
|
||||
|
||||
A native opencode tool `paren-repair` is available at `.opencode/scripts/paren-repair.ts`.
|
||||
The LLM can call it directly with:
|
||||
- `files`: Array of file paths to fix
|
||||
- `code`: Code string to fix via stdin
|
||||
|
||||
Example usage by the LLM:
|
||||
```
|
||||
paren-repair(files="src/foo.clj, src/bar.cljs")
|
||||
paren-repair(code="(defn foo [x")
|
||||
```
|
||||
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
# Psql
|
||||
|
||||
`scripts/psql` is a wrapper around `psql` that connects to the Penpot PostgreSQL
|
||||
database using environment variables (`PENPOT_DB_HOST`, `PENPOT_DB_USER`,
|
||||
`PENPOT_DB_PASSWORD`, `PENPOT_DB_NAME`) with sensible defaults for local
|
||||
development.
|
||||
|
||||
## When to use
|
||||
|
||||
- Running ad-hoc SQL queries against the Penpot database.
|
||||
- Inspecting schema, migrations, or data during development or debugging.
|
||||
|
||||
## How to use (CLI)
|
||||
|
||||
```bash
|
||||
# Default connection (penpot db, localhost)
|
||||
scripts/psql -c "SELECT version();"
|
||||
|
||||
# Test database
|
||||
scripts/psql --test -c "SELECT * FROM migrations;"
|
||||
|
||||
# Custom host/user/database
|
||||
scripts/psql --host myhost --user myuser --db mydb
|
||||
```
|
||||
|
||||
`scripts/psql` must be invoked from the repo root so the path resolves.
|
||||
|
||||
## Native Tool Available (opencode)
|
||||
|
||||
A native opencode tool `penpot-psql` is available. The LLM can call it directly
|
||||
with:
|
||||
- `sql`: SQL command string to execute
|
||||
- `test`: Boolean flag to use the `penpot_test` database
|
||||
|
||||
Example usage by the LLM:
|
||||
```
|
||||
penpot-psql(sql="SELECT version();")
|
||||
penpot-psql(sql="SELECT * FROM migrations;", test=true)
|
||||
```
|
||||
@ -1,44 +0,0 @@
|
||||
# Taiga API client
|
||||
|
||||
`scripts/taiga.py` fetches public issues, user stories, and tasks from the
|
||||
Penpot Taiga project (id 345963) without authentication.
|
||||
|
||||
## When to use
|
||||
|
||||
- Fetching details of a Taiga issue, user story, or task by URL or ref number.
|
||||
- Inspecting status, assignee, tags, description, and other metadata.
|
||||
- Piping structured JSON into other scripts (with `--json`).
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
# Fetch by full Taiga URL
|
||||
python3 scripts/taiga.py https://tree.taiga.io/project/penpot/issue/13714
|
||||
|
||||
# Fetch by type and ref number
|
||||
python3 scripts/taiga.py issue 13714
|
||||
python3 scripts/taiga.py us 14128
|
||||
python3 scripts/taiga.py task 13648
|
||||
|
||||
# Output raw JSON instead of formatted summary
|
||||
python3 scripts/taiga.py --json issue 13714
|
||||
python3 scripts/taiga.py --json https://tree.taiga.io/project/penpot/us/14128
|
||||
```
|
||||
|
||||
## Supported types
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `issue` | Bug reports, feature requests |
|
||||
| `us` | User stories |
|
||||
| `task` | Implementation tasks |
|
||||
|
||||
## Output
|
||||
|
||||
Default output is a formatted summary with title, status, assignee, author,
|
||||
tags, URL, and description. Use `--json` for the raw API response.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.8+ (standard library only, no dependencies).
|
||||
- Network access to `api.taiga.io`.
|
||||
@ -1,178 +0,0 @@
|
||||
# Testing
|
||||
|
||||
## Overview
|
||||
|
||||
Tests are proof that code works. Every behavior change needs a test.
|
||||
|
||||
Testing in this monorepo varies by module. Each module has its own test
|
||||
commands, helpers, runner registration requirements, and conventions. This
|
||||
memory covers cross-cutting testing principles. For module-specific commands
|
||||
and helpers, consult:
|
||||
|
||||
- `mem:common/testing` — CLJC unit tests (JVM + JS), test helpers, fixture
|
||||
builders, production-path change helpers
|
||||
- `mem:frontend/testing` — CLJS unit tests, Playwright E2E integration tests,
|
||||
live browser verification via nREPL
|
||||
- Backend — JVM `clojure.test` under `backend/test/`; see `mem:backend/core`
|
||||
|
||||
## When to Use
|
||||
|
||||
- Implementing new logic or behavior
|
||||
- Fixing any bug (reproduction test required)
|
||||
- Modifying existing functionality
|
||||
- Adding edge case handling
|
||||
|
||||
**When NOT to use:** Pure configuration changes, documentation updates, or
|
||||
static content changes with no behavioral impact.
|
||||
|
||||
## TDD: Recommended Workflow
|
||||
|
||||
Write a failing test before writing the code that makes it pass. For bug fixes,
|
||||
reproduce the bug with a test before attempting a fix.
|
||||
|
||||
When TDD isn't practical (exploratory work, tight coupling to unknown APIs),
|
||||
still write tests before considering the work complete.
|
||||
|
||||
```
|
||||
RED GREEN REFACTOR
|
||||
Write a test Write minimal code Clean up the
|
||||
that fails ──→ to make it pass ──→ implementation ──→ (repeat)
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
Test FAILS Test PASSES Tests still PASS
|
||||
```
|
||||
|
||||
- **RED** — Write the test first. It must fail. A test that passes immediately
|
||||
proves nothing.
|
||||
- **GREEN** — Write the minimum code to make the test pass. Don't over-engineer.
|
||||
- **REFACTOR** — With tests green, improve the code without changing behavior:
|
||||
extract shared logic, improve naming, remove duplication. Run tests after
|
||||
every step.
|
||||
|
||||
## The Prove-It Pattern (Bug Fixes)
|
||||
|
||||
When a bug is reported, **do not start by trying to fix it.** Start by writing
|
||||
a test that reproduces it:
|
||||
|
||||
1. Write a test that demonstrates the bug
|
||||
2. Confirm the test FAILS (proving the bug exists)
|
||||
3. Implement the fix
|
||||
4. Confirm the test PASSES (proving the fix works)
|
||||
5. Run the full test suite for the module (no regressions)
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Test State, Not Interactions** — assert on outcomes, not method calls;
|
||||
survives refactoring
|
||||
- **DAMP over DRY** — tests are specifications; duplication is OK if each test
|
||||
is self-contained and readable. A test should tell a complete story without
|
||||
requiring the reader to trace through shared helpers.
|
||||
- **Prefer Real Implementations** — hierarchy: Real > Fake > Stub > Mock;
|
||||
mock only at boundaries (network, RPC, filesystem, email)
|
||||
- **Arrange-Act-Assert** — every test: setup / action / verify
|
||||
- **One Assertion Per Concept** — each test verifies one behavior; split
|
||||
compound assertions
|
||||
- **Descriptive Test Names** — names read like specifications
|
||||
|
||||
## Prefer Real Implementations Over Mocks
|
||||
|
||||
Work down this list:
|
||||
|
||||
1. **Real implementation** — Test the actual code with real collaborators.
|
||||
Highest confidence.
|
||||
2. **Fake** — A simplified but functional in-memory implementation (e.g.
|
||||
atom/dict-backed store instead of a real database).
|
||||
3. **Stub** — Returns canned data. Use when the collaborator's logic is
|
||||
irrelevant.
|
||||
4. **Mock** — Last resort, only at boundaries. Use only when verifying
|
||||
interaction with an external system that cannot be faked.
|
||||
|
||||
**Rule of thumb:** If you can write a fake or use the real implementation, do
|
||||
that. If you find yourself asserting on call counts or invocation order, ask
|
||||
whether a fake would be clearer.
|
||||
|
||||
## Fixtures over Manual Setup
|
||||
|
||||
Use fixture/`beforeEach` mechanisms for shared setup and teardown. Each test
|
||||
should own its state so tests don't interfere with each other. Shorter-scope
|
||||
fixtures (`:each` / per-test) are preferred; longer-scope fixtures (`:once` /
|
||||
suite-level) are only for expensive, immutable shared setup.
|
||||
|
||||
## Parametrized Tests
|
||||
|
||||
Use your test framework's parametrize/table-driven mechanism to test multiple
|
||||
scenarios with a single test body. Keeps tests concise and surfaces all cases
|
||||
at a glance.
|
||||
|
||||
## Test Pyramid
|
||||
|
||||
```
|
||||
╱╲
|
||||
╱ ╲ E2E (few)
|
||||
╱ ╲ Full flows, real browser/server
|
||||
╱──────╲
|
||||
╱ ╲ Integration (some)
|
||||
╱ ╲ Cross-module, test DB
|
||||
╱────────────╲
|
||||
╱ ╲ Unit (most)
|
||||
╱ ╲ Pure logic, fast
|
||||
╱──────────────────╲
|
||||
```
|
||||
|
||||
Prefer unit tests for pure logic. Reach for integration/E2E tests when covering
|
||||
RPC handlers, database queries, or full user flows. In the frontend, Playwright
|
||||
E2E tests should not be added unless explicitly requested.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Problem | Fix |
|
||||
|---|---|---|
|
||||
| Testing implementation details | Breaks on refactor | Test inputs/outputs |
|
||||
| Flaky tests (timing, order-dependent) | Erodes trust | Deterministic assertions, isolate state |
|
||||
| Mocking everything | Tests pass, production breaks | Prefer real implementations or fakes |
|
||||
| No test isolation | Pass individually, fail together | Per-test state fixtures |
|
||||
| Testing framework/platform code | Wastes time | Only test YOUR code |
|
||||
| Snapshot abuse | Nobody reviews, break on any change | Focused assertions |
|
||||
| Skipping tests to make suite pass | Hides real failures | Fix the test or fix the code |
|
||||
|
||||
## Execution discipline
|
||||
|
||||
**CRITICAL: Test output handling rules**
|
||||
|
||||
When running ANY test command (CLJS/JS or JVM):
|
||||
|
||||
1. **NEVER pipe test output directly to `| head`, `| tail`, `| grep`, or similar filters** — this can hide failures and cause you to miss critical errors.
|
||||
2. **ALWAYS pipe to a file first, then read the file:**
|
||||
```bash
|
||||
# CORRECT:
|
||||
pnpm run test 2>&1 > /tmp/test-output.txt
|
||||
grep -A 5 "failures" /tmp/test-output.txt
|
||||
|
||||
# WRONG:
|
||||
pnpm run test 2>&1 | tail -20
|
||||
pnpm run test 2>&1 | grep "failures"
|
||||
```
|
||||
3. **Use `--focus` to narrow test scope** instead of filtering output.
|
||||
4. **Read the full output file** to understand test results completely.
|
||||
|
||||
When running CLJS/JS tests (frontend, common):
|
||||
- **Always use `pnpm run test:quiet`** — it silently builds the test bundle then runs the test runner, giving you clean test output.
|
||||
- Use `pnpm run test` when you want to see build output alongside test results (always builds, then runs).
|
||||
- After `build:test` has been run once, you can invoke the runner directly: `node target/tests/test.js [--focus ...] [--log-level ...]`.
|
||||
|
||||
When running JVM tests (backend, common):
|
||||
- Use `clojure -M:dev:test` directly (no pnpm wrapper).
|
||||
- Same file-piping rule applies.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After completing any implementation:
|
||||
|
||||
- [ ] Every new behavior has a corresponding test
|
||||
- [ ] All tests pass for touched modules
|
||||
- [ ] Bug fixes include a reproduction test that failed before the fix
|
||||
- [ ] Test names describe the behavior being verified
|
||||
- [ ] No tests were skipped or disabled
|
||||
- [ ] Lint/formatter passes for touched modules
|
||||
- [ ] New test files registered in the module's runner/entrypoint (see module
|
||||
testing memory)
|
||||
@ -15,13 +15,26 @@ automatically pull the identity from the local git config `user.name` and `user.
|
||||
|
||||
Body explaining what changed and why.
|
||||
|
||||
AI-assisted-by: model-name
|
||||
Co-authored-by: <You (the LLM)>
|
||||
```
|
||||
|
||||
**AI-assisted-by trailer rules:**
|
||||
- Use only the model name, e.g. `mimo-v2.5`, `deepseek-v4-flash`
|
||||
- Do NOT add prefixes like `opencode-go/` — use the bare model name
|
||||
|
||||
## Commit Type Emojis
|
||||
|
||||
`:bug:` bug fix · `:sparkles:` enhancement · `:tada:` new feature · `:recycle:` refactor · `:lipstick:` cosmetic · `:ambulance:` critical fix · `:books:` docs · `:construction:` WIP · `:boom:` breaking · `:wrench:` config · `:zap:` perf · `:whale:` docker · `:paperclip:` other · `:arrow_up:` dep upgrade · `:arrow_down:` dep downgrade · `:fire:` removal · `:globe_with_meridians:` translations · `:rocket:` epic/highlight
|
||||
|
||||
|
||||
## Changelogs
|
||||
|
||||
**IMPORTANT:** do not modify the changelog unless it explicitly asked.
|
||||
|
||||
For user-facing or notable changes, update the relevant changelog under the unreleased section:
|
||||
- Main app/modules (`backend`, `frontend`, `common`, `render-wasm`, `exporter`, `mcp`): root `CHANGES.md`.
|
||||
- Plugin subproject changes: `plugins/CHANGELOG.md`.
|
||||
|
||||
Entry format uses the matching category (`:sparkles:`, `:bug:`, etc.) and references the GitHub issue:
|
||||
|
||||
```
|
||||
- Short description of change [#NNNN](https://github.com/penpot/penpot/issues/NNNN)
|
||||
```
|
||||
|
||||
Plugin API changelog prefixes: type/signature -> `**plugin-types:**`; runtime behavior -> `**plugin-runtime:**` in `plugins/CHANGELOG.md`.
|
||||
|
||||
@ -1,355 +0,0 @@
|
||||
# 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** | `community contribution` (PRs from non-core) · skip workflow labels (`backport candidate`, `team-qa`) · do **not** add `bug` or `enhancement` labels (use Issue Type instead) |
|
||||
| **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>
|
||||
```
|
||||
|
||||
Note: do not soft-wrap paragraphs in the body. Each paragraph is a single line in the source; newlines are reserved for structural breaks (section headers, list items, code-block fences, blank-line separators). List items stay on a single line each. GitHub renders single-line paragraphs correctly, and wrapping makes diffs noisy on every small wording change. Same rule applies to PR bodies.
|
||||
|
||||
## 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 report (steps to reproduce, expected vs. actual) → Bug
|
||||
- Enhancement / new feature → 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
|
||||
```
|
||||
|
||||
## Creating Issues from PRs
|
||||
|
||||
Used when the project board needs an issue as the primary changelog/release
|
||||
unit and the PR describes the implementation. The issue is the **WHAT**
|
||||
(user-facing), the PR is the **HOW** (implementation).
|
||||
|
||||
### Fetch the PR
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot \
|
||||
--json title,body,author,labels,baseRefName,mergedAt,state,milestone
|
||||
```
|
||||
|
||||
Identify:
|
||||
|
||||
- **WHAT** — user-facing problem or feature. Goes into the issue.
|
||||
Describe symptoms and impact, not internal mechanisms.
|
||||
- **HOW** — implementation details. These belong in the PR, not the issue.
|
||||
|
||||
### Determine metadata
|
||||
|
||||
- **Title:** rewrite from user perspective using the title rules above. Strip
|
||||
leading emoji prefixes (`:bug:`, `:sparkles:`, `:tada:`). Focus on
|
||||
observable behavior.
|
||||
- **Labels:** copy `community contribution` if present on the PR.
|
||||
- **Milestone:** always copy what's on the PR.
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --json milestone --jq '.milestone.title'
|
||||
```
|
||||
|
||||
If the PR has no milestone, create the issue without one.
|
||||
- **Project:** `Main`.
|
||||
- **Body:** extract the user-facing section (steps to reproduce or feature
|
||||
description). Omit internal details. Use the templates above.
|
||||
- **Issue Type:** use the mapping table above (also handles `:bug:` /
|
||||
`:sparkles:` / `:tada:` title prefixes).
|
||||
|
||||
### Create the issue
|
||||
|
||||
```bash
|
||||
cat > /tmp/issue-body.md << 'ISSUE_BODY'
|
||||
<body content here>
|
||||
ISSUE_BODY
|
||||
|
||||
gh issue create \
|
||||
--repo penpot/penpot \
|
||||
--title "<Title>" \
|
||||
--label "community contribution" \ # only if PR has this label
|
||||
--milestone "<milestone>" \
|
||||
--project "Main" \
|
||||
--body-file /tmp/issue-body.md
|
||||
```
|
||||
|
||||
Output: `https://github.com/penpot/penpot/issues/<NUMBER>`
|
||||
|
||||
### Assign to the PR author
|
||||
|
||||
```bash
|
||||
AUTHOR=$(gh pr view <PR_NUMBER> --repo penpot/penpot --json author --jq '.author.login')
|
||||
gh issue edit <ISSUE_NUMBER> --repo penpot/penpot --add-assignee "$AUTHOR"
|
||||
```
|
||||
|
||||
### Set Issue Type and verify
|
||||
|
||||
See the **Setting the Issue Type** and **Verification** sections above — the
|
||||
GraphQL mutations and `gh issue view` calls are identical regardless of how
|
||||
the issue was sourced.
|
||||
|
||||
### Link the PR to the issue
|
||||
|
||||
Append `Closes #<ISSUE_NUMBER>` to the PR body:
|
||||
|
||||
```bash
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot --json body --jq '.body' > /tmp/pr-body.md
|
||||
printf "\n\nCloses #<ISSUE_NUMBER>\n" >> /tmp/pr-body.md
|
||||
gh pr edit <PR_NUMBER> --repo penpot/penpot --body-file /tmp/pr-body.md
|
||||
|
||||
# Verify
|
||||
gh pr view <PR_NUMBER> --repo penpot/penpot --json body \
|
||||
--jq '.body | test("Closes #<ISSUE_NUMBER>")'
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
### Clean up
|
||||
|
||||
```bash
|
||||
rm -f /tmp/issue-body.md /tmp/pr-body.md
|
||||
```
|
||||
|
||||
### Rules for this flow
|
||||
|
||||
- **One issue per PR** — even if a PR fixes multiple things, create a single
|
||||
issue that summarizes the overall change.
|
||||
- **Community attribution:** if the PR has the `community contribution`
|
||||
label or the author is not a core team member, add the label to the issue.
|
||||
- **Don't put implementation details in the issue body** — the issue is for
|
||||
users, QA, and changelog readers.
|
||||
|
||||
## Creating Issues from Draft Body
|
||||
|
||||
Used when the user provides a draft body from elsewhere (Taiga story, user
|
||||
report, discussion transcript) and there is no PR yet.
|
||||
|
||||
### Get the body
|
||||
|
||||
Read the draft body from wherever it was provided. If the user gives only a
|
||||
vague one-liner, ask them to expand it (steps to reproduce, expected vs.
|
||||
actual, use case) before proceeding.
|
||||
|
||||
### Derive the title
|
||||
|
||||
Apply the title rules in the **Title Derivation** section above. Distinguish
|
||||
bug vs. feature from the body content:
|
||||
|
||||
- Steps to reproduce + expected vs. actual → bug
|
||||
- "would be nice", "add support for", "allow users to" → feature / enhancement
|
||||
|
||||
### Choose a body template
|
||||
|
||||
Use the bug or enhancement template from the **Issue Body Template** section
|
||||
above. Fill in placeholders with the user-provided details. If the body
|
||||
doesn't fit either, ask the user which template to use.
|
||||
|
||||
### Determine metadata
|
||||
|
||||
- **Project:** `Main` (always).
|
||||
- **Milestone:** ask the user if not obvious; otherwise omit.
|
||||
- **Labels:** usually none for new user-reported issues. Add
|
||||
`community contribution` if the user is a non-team contributor.
|
||||
- **Issue Type:** use the mapping table above (bug description → Bug; feature
|
||||
request → Enhancement or Feature).
|
||||
|
||||
### Create the issue
|
||||
|
||||
```bash
|
||||
cat > /tmp/issue-body.md << 'ISSUE_BODY'
|
||||
<body content here>
|
||||
ISSUE_BODY
|
||||
|
||||
gh issue create \
|
||||
--repo penpot/penpot \
|
||||
--title "<Title>" \
|
||||
--label "community contribution" \ # only if applicable
|
||||
--milestone "<milestone>" \ # only if provided
|
||||
--project "Main" \
|
||||
--body-file /tmp/issue-body.md
|
||||
```
|
||||
|
||||
### Set Issue Type and verify
|
||||
|
||||
Same GraphQL mutation and `gh issue view` commands as in the
|
||||
**Setting the Issue Type** and **Verification** sections above.
|
||||
|
||||
### Clean up
|
||||
|
||||
```bash
|
||||
rm -f /tmp/issue-body.md
|
||||
```
|
||||
|
||||
## Retitling an Existing Issue
|
||||
|
||||
Used when an issue's current title is vague, prefixed, or no longer matches
|
||||
the body (e.g. `[PENPOT FEEDBACK]: ...`, `feature: ...`).
|
||||
|
||||
### Fetch the issue
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo penpot/penpot --json title,body
|
||||
```
|
||||
|
||||
### Derive a new title
|
||||
|
||||
Read the body (not the current title) and apply the title rules in the
|
||||
**Title Derivation** section above.
|
||||
|
||||
### Apply the new title
|
||||
|
||||
```bash
|
||||
gh issue edit <NUMBER> --repo penpot/penpot --title "<NEW TITLE>"
|
||||
```
|
||||
|
||||
### Confirm
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo penpot/penpot --json title
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- End-to-end orchestration entry point: the `create-issue` skill at
|
||||
`.opencode/skills/create-issue/SKILL.md`. The skill is a thin entry
|
||||
point; this memory is the canonical home for all issue-creation rules.
|
||||
@ -2,20 +2,6 @@
|
||||
|
||||
PR only on explicit request. Branch: issue/feature-specific; fallback `<type>/<short-description>` (`fix/...`, `feat/...`, `refactor/...`, `docs/...`, `chore/...`, `perf/...`).
|
||||
|
||||
## Target Branch
|
||||
|
||||
Auto-detect the base branch with `scripts/detect-target-branch`:
|
||||
|
||||
```bash
|
||||
TARGET=$(scripts/detect-target-branch)
|
||||
```
|
||||
|
||||
This outputs `staging` or `develop` by walking the local commit graph (pure local, no remote/network). Do not ask the user for the target branch unless the tool fails.
|
||||
|
||||
## Metadata
|
||||
|
||||
Always add the PR to the Main project (`--project "Main"`) unless the user explicitly requests a different project.
|
||||
|
||||
## Title Format
|
||||
|
||||
PR titles follow commit title conventions:
|
||||
@ -26,74 +12,21 @@ PR titles follow commit title conventions:
|
||||
|
||||
See `mem:workflow/creating-commits` for emoji codes. Squash merge uses the PR title as the final commit subject, so title format matters.
|
||||
|
||||
## Description Body
|
||||
## Description
|
||||
|
||||
Include concise sections covering:
|
||||
- what changed and why;
|
||||
- related GitHub issues or Taiga stories (`Closes #NNNN`, `Relates to #NNNN`, `Taiga #NNNN`);
|
||||
- related GitHub issues or Taiga stories (`Fixes #NNNN`, `Relates to #NNNN`, `Taiga #NNNN`);
|
||||
- screenshots or recordings for UI-visible changes;
|
||||
- testing performed and residual risk;
|
||||
- breaking changes or migration notes, if any.
|
||||
|
||||
PR descriptions follow this structure:
|
||||
PR descriptions are expected to start with:
|
||||
|
||||
```markdown
|
||||
**Note:** This PR was created with AI assistance.
|
||||
|
||||
## What
|
||||
|
||||
<the problem or feature and its user-facing impact — short bullet items where there is more than one point>
|
||||
|
||||
## Why
|
||||
|
||||
<root cause or motivation — a short paragraph or bullets>
|
||||
|
||||
## How
|
||||
|
||||
<high-level approach and key decisions — bullet items, grouped by area (bold lead-ins) for larger PRs>
|
||||
```
|
||||
|
||||
The "Note:" line is required at the top. Adjust if this is a manual (non-AI) PR.
|
||||
|
||||
## Writing Principles
|
||||
|
||||
- **Write for humans.** The diff shows what changed. The description explains why.
|
||||
- **Be concise.** Focus on reasoning: What was the problem? Why did it happen? How did you solve it?
|
||||
- **Prefer bullets over paragraphs.** Short bullet items, grouped by area with bold lead-ins where helpful, are far easier to digest than prose; keep any remaining paragraph to a few sentences.
|
||||
- **No manual line wraps.** Markdown renders adapting to the viewport; hard-wrapped lines degrade rendering. One line per paragraph or bullet, however long.
|
||||
- **Skip the obvious.** Don't explain what `git diff` already shows.
|
||||
|
||||
### What NOT to Include
|
||||
|
||||
- ❌ List of files changed (visible in diff)
|
||||
- ❌ Testing steps (CI handles this)
|
||||
- ❌ Screenshots unless UI-visible
|
||||
- ❌ Migration notes unless breaking changes
|
||||
- ❌ Regression fixes introduced during the PR (they're part of the development process, not the feature)
|
||||
> **Note:** This PR was created with AI assistance as part of the Penpot MCP self-improvement initiative.
|
||||
|
||||
## Before Opening
|
||||
|
||||
- Follow `mem:workflow/creating-commits` for commits
|
||||
- Follow `mem:workflow/creating-commits` for changelog expectations.
|
||||
- Run the focused tests/lints appropriate to touched modules.
|
||||
- Do not force-push during review unless the maintainer workflow explicitly asks for it.
|
||||
- When the user says the code is already pushed, trust that — do not verify remote branch existence via `git ls-remote` or `git fetch`.
|
||||
|
||||
## Creating the PR
|
||||
|
||||
```bash
|
||||
cat > /tmp/pr-body.md << 'PR_BODY'
|
||||
<body content here>
|
||||
PR_BODY
|
||||
|
||||
TARGET=$(scripts/detect-target-branch)
|
||||
|
||||
gh pr create \
|
||||
--repo penpot/penpot \
|
||||
--base "$TARGET" \
|
||||
--head <branch> \
|
||||
--title "<title>" \
|
||||
--project "Main" \
|
||||
--body-file /tmp/pr-body.md
|
||||
|
||||
rm -f /tmp/pr-body.md
|
||||
```
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Docs
|
||||
# Docs Workflow
|
||||
|
||||
`docs/`: Penpot documentation site; Eleventy.
|
||||
|
||||
@ -1,31 +1,26 @@
|
||||
# the name by which the project can be referenced within Serena/when chatting with the LLM.
|
||||
# the name by which the project can be referenced within Serena
|
||||
project_name: "penpot"
|
||||
|
||||
# list of languages for which language servers are started (LSP backend only); choose from:
|
||||
# ada al angular ansible bash
|
||||
# bsl clojure cpp cpp_ccls crystal
|
||||
# csharp csharp_omnisharp cue dart elixir
|
||||
# elm erlang fortran fsharp gdscript
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al ansible bash clojure cpp
|
||||
# cpp_ccls crystal csharp csharp_omnisharp dart
|
||||
# elixir elm erlang fortran fsharp
|
||||
# go groovy haskell haxe hlsl
|
||||
# html java json julia kotlin
|
||||
# latex lean4 lua luau markdown
|
||||
# matlab msl nix ocaml pascal
|
||||
# perl php php_phpactor php_phpantom powershell
|
||||
# python python_jedi python_pyrefly python_ty r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# scss solidity svelte swift systemverilog
|
||||
# terraform toml typescript typescript_vts vue
|
||||
# yaml zig
|
||||
# (This list may be outdated; generated with scripts/print_language_list.py;
|
||||
# For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py)
|
||||
# java json julia kotlin lean4
|
||||
# lua luau markdown matlab msl
|
||||
# nix ocaml pascal perl php
|
||||
# php_phpactor powershell python python_jedi python_ty
|
||||
# r rego ruby ruby_solargraph rust
|
||||
# scala solidity swift systemverilog terraform
|
||||
# toml typescript typescript_vts vue yaml
|
||||
# zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
|
||||
# - For Svelte projects, use svelte (subsumes typescript/javascript for .svelte projects; requires npm)
|
||||
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
@ -59,19 +54,12 @@ ignore_all_files_in_gitignore: true
|
||||
|
||||
# advanced configuration option allowing to configure language server-specific options.
|
||||
# Maps the language key to the options.
|
||||
# The settings are considered only if the project is trusted (see global configuration to define trusted projects).
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#language-server-specific-settings
|
||||
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
||||
# No documentation on options means no options are available.
|
||||
ls_specific_settings: {}
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Important: quote patterns that start with `*`, otherwise YAML treats them as aliases.
|
||||
# Example:
|
||||
# ignored_paths:
|
||||
# - "examples/**"
|
||||
# - ".worktrees/**"
|
||||
# - "**/bin/**"
|
||||
# - "**/obj/**"
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
@ -142,38 +130,13 @@ ignored_memory_patterns: []
|
||||
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
||||
added_modes:
|
||||
|
||||
# list of additional workspace folder paths for cross-package reference support.
|
||||
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
|
||||
# Paths can be absolute or relative to the project root.
|
||||
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
|
||||
# symbols and references across package boundaries, but these folders are not indexed by Serena,
|
||||
# i.e. the respective symbols will not be found using Serena's symbol search tools.
|
||||
# symbols and references across package boundaries.
|
||||
# Currently supported for: TypeScript.
|
||||
# Example:
|
||||
# additional_workspace_folders:
|
||||
# - ../sibling-package
|
||||
# - ../shared-lib
|
||||
ls_additional_workspace_folders: []
|
||||
|
||||
# list of workspace folder paths (LSP backend only).
|
||||
# These folders will be used to build up Serena's symbol index.
|
||||
# Paths must be within the project root and should thus be relative to the project root.
|
||||
# Furthermore, the paths should not be filtered by ignore settings.
|
||||
# Default setting: The entire project root folder (".") is considered.
|
||||
# In (large) monorepos, this can be used to index only subfolders of the project root, e.g.
|
||||
# ls_workspace_folders:
|
||||
# - "./subproject1"
|
||||
# - "./subproject2"
|
||||
ls_workspace_folders:
|
||||
- .
|
||||
|
||||
# optional shell command to run before the language backend (LSP or JetBrains) is initialised.
|
||||
# the command runs in the project root directory and is only executed if the project is trusted
|
||||
# (see trusted_project_path_patterns in the global configuration).
|
||||
# serena waits for the command to exit: a non-zero exit code is logged as an error but does not
|
||||
# abort activation. a per-project timeout (activation_command_timeout, default 180s) is the safety
|
||||
# backstop for non-terminating commands; on expiry the process is killed and activation continues.
|
||||
# example: activation_command: "npx nx run-many -t build"
|
||||
activation_command:
|
||||
|
||||
# maximum time in seconds to wait for activation_command to complete before killing it (default 180s).
|
||||
# must be a positive number.
|
||||
activation_command_timeout: 180.0
|
||||
additional_workspace_folders: []
|
||||
|
||||
174
AGENTS.md
174
AGENTS.md
@ -1,116 +1,104 @@
|
||||
# AI AGENT GUIDE
|
||||
# AI Agent Guide
|
||||
|
||||
## HARD RULES (always apply — no exceptions)
|
||||
This document provides the core context and operating guidelines for AI agents
|
||||
working in this repository.
|
||||
|
||||
- **Never `git push`, force-push, or modify `git origin`** (or any other remote).
|
||||
The user pushes from their own shell. If a push is required to surface the
|
||||
agent's work (e.g. force-push after an amend), state this in the response and
|
||||
wait for the user to push. Do not change the remote URL, do not switch SSH↔HTTPS.
|
||||
- **Never amend a commit that has been pushed** unless the user explicitly asks.
|
||||
If the user pushes, treat that commit as final from the agent's side.
|
||||
- **Never pipe test output directly to filters** (`| head`, `| tail`, `| grep`, etc.).
|
||||
Always redirect to a file first: `command > /tmp/output.txt 2>&1`, then read/grep the file.
|
||||
This prevents hiding test failures. See `mem:testing` for details.
|
||||
- **Read the workflow memory BEFORE the corresponding action**:
|
||||
- Before `git commit` → `mem:workflow/creating-commits` (commit format, AI-assisted-by trailer)
|
||||
- Before `gh issue create` → `mem:workflow/creating-issues` (title derivation, body template, Issue Type)
|
||||
- Before `gh pr create` / `gh pr edit` → `mem:workflow/creating-prs` (title format, body structure, AI note)
|
||||
Don't infer format from the title of a previous commit/issue/PR — the memory
|
||||
is the source of truth.
|
||||
## Before You Start
|
||||
|
||||
## CRITICAL: Read module memories BEFORE writing any code
|
||||
Before responding to any user request, you must:
|
||||
|
||||
Do this **before planning, before coding, before touching any file**:
|
||||
1. Read this file completely.
|
||||
2. Identify which modules are affected by the task.
|
||||
3. Load the `AGENTS.md` file **only** for each affected module (see the
|
||||
architecture table below). Not all modules have an `AGENTS.md` — verify the
|
||||
file exists before attempting to read it.
|
||||
4. Do **not** load `AGENTS.md` files for unrelated modules.
|
||||
|
||||
1. Read `critical-info` (use `serena_read_memory critical-info` or read `.serena/memories/critical-info.md`).
|
||||
It describes the project structure and tells you which modules exist.
|
||||
2. From `critical-info`, identify which modules your task affects.
|
||||
3. Read each affected module's **core memory** — the name is `<module>/core`
|
||||
(e.g. `frontend/core`, `backend/core`, `common/core`).
|
||||
4. If the core memory references deeper `mem:` memories relevant to your task, read those too.
|
||||
|
||||
**STOP: Do not proceed until you have read the core memory of every affected module.**
|
||||
Skipping this step is the #1 cause of incorrect or incomplete work.
|
||||
|
||||
---
|
||||
|
||||
# Memory system
|
||||
|
||||
Memories are the **primary project guidance** — not docs or readme files.
|
||||
They are dense, agent-oriented notes: terse bullets, invariants, no prose.
|
||||
|
||||
## Entry point
|
||||
|
||||
Start at `critical-info` (the graph root). It describes the project structure,
|
||||
module dependency graph, and references section-level core memories.
|
||||
|
||||
## Progressive discovery model
|
||||
|
||||
Memories form a **reference graph**, not a flat list:
|
||||
|
||||
```
|
||||
critical-info ← read first (graph root)
|
||||
└─ <section>/core ← top-level memory per section (e.g. frontend/core, backend/core)
|
||||
└─ <topic> ← focused memories (e.g. frontend/handling-errors-and-debugging)
|
||||
└─ ... ← deeper memories as needed
|
||||
```
|
||||
|
||||
When working on a task:
|
||||
1. Read `critical-info` to identify which sections are affected.
|
||||
2. Read the affected section's `core` memory for an overview.
|
||||
3. Follow `mem:` references in the core memory to focused memories relevant to your task.
|
||||
4. Continue following references deeper as needed.
|
||||
|
||||
## Accessing memories
|
||||
|
||||
- **If `serena_read_memory` / `serena_list_memories` tools are available**: use them.
|
||||
`serena_read_memory` takes a memory name (e.g. `critical-info`, `frontend/core`).
|
||||
- **If tools are NOT available**: read the filesystem directly.
|
||||
Memory name `mem:foo/bar` maps to file `.serena/memories/foo/bar.md`.
|
||||
|
||||
## Cross-reference convention
|
||||
|
||||
Memories reference other memories with `mem:<section>/<name>` inside backticks.
|
||||
Example: `mem:common/changes-architecture`.
|
||||
When you encounter a `mem:` reference relevant to your task, read that memory next.
|
||||
|
||||
## Topic/folder organization
|
||||
|
||||
Memories are grouped into folders that mirror project modules or topics:
|
||||
`backend/`, `common/`, `frontend/`, `render-wasm/`, `exporter/`, `workflow/`, etc.
|
||||
Each folder's top-level memory is `<folder>/core`.
|
||||
|
||||
---
|
||||
|
||||
# Role: Senior Software Engineer
|
||||
## Role: Senior Software Engineer
|
||||
|
||||
You are a high-autonomy Senior Full-Stack Software Engineer. You have full
|
||||
permission to navigate the codebase, modify files, and execute commands to
|
||||
fulfill your tasks. Your goal is to solve complex technical tasks with high
|
||||
precision while maintaining a strong focus on maintainability and performance.
|
||||
|
||||
## Operational Guidelines
|
||||
### Operational Guidelines
|
||||
|
||||
1. Before writing code, describe your plan. If the task is complex, break it
|
||||
down into atomic steps.
|
||||
2. Be concise and autonomous.
|
||||
3. Do **not** touch unrelated modules unless the task explicitly requires it.
|
||||
4. Commit only when explicitly asked. Follow the commit format rules in
|
||||
`CONTRIBUTING.md`.
|
||||
5. When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects
|
||||
`.gitignore` by default.
|
||||
|
||||
---
|
||||
## Changelogs
|
||||
|
||||
# Available Scripts & Tools
|
||||
The project has two changelogs:
|
||||
|
||||
## Native opencode Tools (callable directly by the LLM)
|
||||
- **Main project changelog**: `CHANGES.md` (root of the repository). Tracks changes for the core Penpot application (backend, frontend, common, render-wasm, exporter, mcp).
|
||||
- **Plugins changelog**: `plugins/CHANGELOG.md`. Tracks changes for the plugins subproject only.
|
||||
|
||||
- `paren-repair` — Fix mismatched delimiters + reformat Clojure files. Example: `paren-repair(files="src/foo.clj, src/bar.cljs")`
|
||||
- `penpot-psql` — Execute SQL against the Penpot database. Example: `penpot-psql(sql="SELECT version();")`
|
||||
When making changes, add a changelog entry to the appropriate file under the
|
||||
`## <version> (Unreleased)` section in the correct category
|
||||
(`:sparkles: New features & Enhancements` or `:bug: Bugs fixed`).
|
||||
|
||||
## Scripts (from repo root via `scripts/<name>`)
|
||||
## GitHub Operations
|
||||
|
||||
- `scripts/paren-repair` — Fix mismatched delimiters in Clojure/CLJS files + reformat with cljfmt. See `mem:scripts/paren-repair`.
|
||||
- `scripts/psql` — Connect to the Penpot PostgreSQL database (wraps `psql` with env-var defaults). See `mem:scripts/psql`.
|
||||
- `scripts/nrepl-eval.mjs` — Evaluate Clojure code via nREPL (backend + frontend).
|
||||
- `scripts/check-commit` — Validate commit messages against Penpot's commit guidelines.
|
||||
- `scripts/check-fmt-clj` — Check Clojure formatting without modifying files.
|
||||
- `scripts/ci` — CI orchestration script for running lint, tests, and format checks across modules. See `scripts/ci --help`.
|
||||
To obtain the list of repository members/collaborators:
|
||||
|
||||
```bash
|
||||
gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login'
|
||||
```
|
||||
|
||||
To obtain the list of open PRs authored by members:
|
||||
|
||||
```bash
|
||||
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
|
||||
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
|
||||
($members | split("|")) as $m |
|
||||
.[] | select(.author.login as $a | $m | index($a)) |
|
||||
"\(.number)\t\(.author.login)\t\(.title)"
|
||||
'
|
||||
```
|
||||
|
||||
To obtain the list of open PRs from external contributors (non-members):
|
||||
|
||||
```bash
|
||||
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
|
||||
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
|
||||
($members | split("|")) as $m |
|
||||
.[] | select(.author.login as $a | $m | index($a) | not) |
|
||||
"\(.number)\t\(.author.login)\t\(.title)"
|
||||
'
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Penpot is an open-source design tool composed of several modules:
|
||||
|
||||
| Directory | Language | Purpose | Has `AGENTS.md` |
|
||||
|-----------|----------|---------|:----------------:|
|
||||
| `frontend/` | ClojureScript + SCSS | Single-page React app (design editor) | Yes |
|
||||
| `backend/` | Clojure (JVM) | HTTP/RPC server, PostgreSQL, Redis | Yes |
|
||||
| `common/` | Cljc (shared Clojure/ClojureScript) | Data types, geometry, schemas, utilities | Yes |
|
||||
| `render-wasm/` | Rust -> WebAssembly | High-performance canvas renderer (Skia) | Yes |
|
||||
| `exporter/` | ClojureScript (Node.js) | Headless Playwright-based export (SVG/PDF) | No |
|
||||
| `mcp/` | TypeScript | Model Context Protocol integration | No |
|
||||
| `plugins/` | TypeScript | Plugin runtime and example plugins | No |
|
||||
|
||||
Some submodules use `pnpm` workspaces. The root `package.json` and
|
||||
`pnpm-lock.yaml` manage shared dependencies. Helper scripts live in `scripts/`.
|
||||
|
||||
### Module Dependency Graph
|
||||
|
||||
```
|
||||
frontend ──> common
|
||||
backend ──> common
|
||||
exporter ──> common
|
||||
frontend ──> render-wasm (loads compiled WASM)
|
||||
```
|
||||
|
||||
`common` is referenced as a local dependency (`{:local/root "../common"}`) by
|
||||
both `frontend` and `backend`. Changes to `common` can therefore affect multiple
|
||||
modules — test across consumers when modifying shared code.
|
||||
|
||||
239
CHANGES.md
239
CHANGES.md
@ -1,220 +1,10 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.18.0 (Unreleased)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix MCP integration hanging when the Penpot tab is backgrounded or frozen by the browser [#10323](https://github.com/penpot/penpot/issues/10323) (PR: [#10392](https://github.com/penpot/penpot/pull/10392))
|
||||
- Fix synced component copy not reflowing children after spacing token update [#9892](https://github.com/penpot/penpot/issues/9892)
|
||||
- Fix spacebar activating pan mode while typing a comment (by @Krishcode264) [#10285](https://github.com/penpot/penpot/issues/10285) (PR: [#10287](https://github.com/penpot/penpot/pull/10287))
|
||||
- Fix plugin API rejecting negative letterSpacing values (by @filipsajdak) [#9780](https://github.com/penpot/penpot/issues/9780) (PR: [#10257](https://github.com/penpot/penpot/pull/10257))
|
||||
- Fix plugin API addTheme calls failing with the signature shown in the high-level overview [#10074](https://github.com/penpot/penpot/issues/10074) (PR: [#10359](https://github.com/penpot/penpot/pull/10359))
|
||||
- Fix empty text shape not being deleted on editor exit [#10540](https://github.com/penpot/penpot/issues/10540) (PR: [#10541](https://github.com/penpot/penpot/pull/10541))
|
||||
- Fix broken token pills showing wrong default state when not selected [#10524](https://github.com/penpot/penpot/issues/10524) (PR: [#10535](https://github.com/penpot/penpot/pull/10535))
|
||||
- Replace hyphens with bullets in subscription benefits list [#10547](https://github.com/penpot/penpot/issues/10547) (PR: [#10523](https://github.com/penpot/penpot/pull/10523))
|
||||
- Fix Chinese (zh-CN) translation showing wrong label for Intersection in board path menu (by @sawirricardo) [#10346](https://github.com/penpot/penpot/issues/10346) (PR: [#10381](https://github.com/penpot/penpot/pull/10381))
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Group toolbar drawing tools into shape and free-draw flyouts [#9316](https://github.com/penpot/penpot/issues/9316) (PR: [#9480](https://github.com/penpot/penpot/pull/9480), [#10354](https://github.com/penpot/penpot/pull/10354))
|
||||
- Add outline stroke to Paths [#9961](https://github.com/penpot/penpot/issues/9961) (PR: [#8677](https://github.com/penpot/penpot/pull/8677))
|
||||
- Make throwValidationErrors default to true for v2 manifest plugins [#10401](https://github.com/penpot/penpot/issues/10401) (PR: [#10433](https://github.com/penpot/penpot/pull/10433))
|
||||
- Add dedicated Line and Arrow drawing tools (by @davidv399) [#9145](https://github.com/penpot/penpot/issues/9145) (PR: [#9146](https://github.com/penpot/penpot/pull/9146))
|
||||
- Refactor wasm rulers and UI state [#10116](https://github.com/penpot/penpot/issues/10116) (PR: [#10461](https://github.com/penpot/penpot/pull/10461))
|
||||
- Improve team invitations modal in the dashboard [#10484](https://github.com/penpot/penpot/issues/10484) (PR: [#10459](https://github.com/penpot/penpot/pull/10459))
|
||||
|
||||
|
||||
## 2.17.1 (Unreleased)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix malformed get-font-variants request when team-id is missing from dashboard URL [#10644](https://github.com/penpot/penpot/issues/10644) (PR: [#10645](https://github.com/penpot/penpot/pull/10645))
|
||||
- Fix malformed get-profiles-for-file-comments request when file-id is missing from workspace URL [#10652](https://github.com/penpot/penpot/issues/10652) (PR: [#10655](https://github.com/penpot/penpot/pull/10655))
|
||||
- Fix workspace crash when holding an arrow key on a selection due to excessive re-renders [#10726](https://github.com/penpot/penpot/issues/10726) (PR: [#10736](https://github.com/penpot/penpot/pull/10736))
|
||||
- Fix asset download failing with S3 auth conflict when using access token [#10776](https://github.com/penpot/penpot/issues/10776) (PR: [#10777](https://github.com/penpot/penpot/pull/10777))
|
||||
- Fix internal error when dragging inner layout with Boolean operations [#10647](https://github.com/penpot/penpot/issues/10647) (PR: [#10778](https://github.com/penpot/penpot/pull/10778))
|
||||
- Fix viewer crash with WASM panic when opening URL with page-id [#10800](https://github.com/penpot/penpot/issues/10800) (PR: [#10805](https://github.com/penpot/penpot/pull/10805))
|
||||
|
||||
## 2.17.0
|
||||
|
||||
### :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))
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Expose `variants` retrieval on `LibraryComponent` via `isVariant()` type guard in plugin API (by @opcode81) [#9185](https://github.com/penpot/penpot/issues/9185) (PR: [#9302](https://github.com/penpot/penpot/pull/9302))
|
||||
- Add search bar to prototype interaction destination dropdown (by @EvaMarco) [#8618](https://github.com/penpot/penpot/issues/8618) (PR: [#9769](https://github.com/penpot/penpot/pull/9769))
|
||||
- Add dashed stroke customization with dash and gap inputs (by @EvaMarco) [#3881](https://github.com/penpot/penpot/issues/3881) (PR: [#9765](https://github.com/penpot/penpot/pull/9765))
|
||||
- Add author, relative timestamp and short identifier to history entries (by @FairyPigDev) [#7660](https://github.com/penpot/penpot/issues/7660) (PR: [#9132](https://github.com/penpot/penpot/pull/9132))
|
||||
- Add typography token row to multiselected texts [#9336](https://github.com/penpot/penpot/issues/9336) (PR: [#9128](https://github.com/penpot/penpot/pull/9128))
|
||||
- Optimize propagation of tokens [#9261](https://github.com/penpot/penpot/issues/9261) (PR: [#9144](https://github.com/penpot/penpot/pull/9144))
|
||||
- Add typography information to token dropdown option [#9377](https://github.com/penpot/penpot/issues/9377) (PR: [#9375](https://github.com/penpot/penpot/pull/9375))
|
||||
- Cache OIDC provider records to skip per-login discovery (by @Dexterity104) [#9294](https://github.com/penpot/penpot/issues/9294) (PR: [#9295](https://github.com/penpot/penpot/pull/9295))
|
||||
- Validate shape on add-object to catch malformed inputs early (by @Dexterity104) [#9507](https://github.com/penpot/penpot/issues/9507) (PR: [#9291](https://github.com/penpot/penpot/pull/9291))
|
||||
- Remove unreachable try/catch in hex->hsl (by @Dexterity104) [#9244](https://github.com/penpot/penpot/issues/9244) (PR: [#9245](https://github.com/penpot/penpot/pull/9245))
|
||||
- Remove stray debug log in exporter upload-resource (by @iot2edge) [#9270](https://github.com/penpot/penpot/issues/9270) (PR: [#9272](https://github.com/penpot/penpot/pull/9272))
|
||||
- Release pool connection during font variant creation (by @Dexterity104) [#9286](https://github.com/penpot/penpot/issues/9286) (PR: [#9287](https://github.com/penpot/penpot/pull/9287))
|
||||
- Add autocomplete combobox to token creation and edition forms [#9899](https://github.com/penpot/penpot/issues/9899) (PR: [#9109](https://github.com/penpot/penpot/pull/9109), [#8294](https://github.com/penpot/penpot/pull/8294))
|
||||
- Add list view mode to color picker UI [#4420](https://github.com/penpot/penpot/issues/4420) (PR: [#9953](https://github.com/penpot/penpot/pull/9953))
|
||||
- Use Clipboard API consistently across the application (by @MilosM348) [#6514](https://github.com/penpot/penpot/issues/6514) (PR: [#9188](https://github.com/penpot/penpot/pull/9188))
|
||||
- Use `$` as DTCG token/group discriminator and make `$description` optional [#8342](https://github.com/penpot/penpot/issues/8342) (PR: [#9912](https://github.com/penpot/penpot/pull/9912))
|
||||
- Match version preview banner text to History sidebar labels (by @MilosM348) [#9503](https://github.com/penpot/penpot/issues/9503) (PR: [#9697](https://github.com/penpot/penpot/pull/9697))
|
||||
- Use "copia" as duplicate suffix for Spanish (by @Rene0422) [#9623](https://github.com/penpot/penpot/issues/9623) (PR: [#9671](https://github.com/penpot/penpot/pull/9671))
|
||||
- Harden CORS middleware to not reflect Origin with credentials enabled [#9659](https://github.com/penpot/penpot/issues/9659) (PR: [#9675](https://github.com/penpot/penpot/pull/9675))
|
||||
- Revert token migrations on clashing names to prevent data loss [#9816](https://github.com/penpot/penpot/issues/9816) (PR: [#9950](https://github.com/penpot/penpot/pull/9950))
|
||||
- Update contributing guidelines with current issue tags and CSS linting rules [#9900](https://github.com/penpot/penpot/issues/9900) (PR: [#9418](https://github.com/penpot/penpot/pull/9418))
|
||||
- Add composite typography token input to the Design sidebar [#9932](https://github.com/penpot/penpot/issues/9932) (PR: [#9128](https://github.com/penpot/penpot/pull/9128), [#9375](https://github.com/penpot/penpot/pull/9375), [#8749](https://github.com/penpot/penpot/pull/8749))
|
||||
- Avoid deduplicating temporary export files to prevent stale content (by @yong2bba) [#9970](https://github.com/penpot/penpot/issues/9970) (PR: [#9959](https://github.com/penpot/penpot/pull/9959))
|
||||
- Add layer blur effect [#9844](https://github.com/penpot/penpot/issues/9844) (PR: [#10034](https://github.com/penpot/penpot/pull/10034))
|
||||
- Add concurrency limiter for MCP Server Plugin Communications [#9493](https://github.com/penpot/penpot/issues/9493) (PR: [#9748](https://github.com/penpot/penpot/pull/9748))
|
||||
- Render guides in WebGL [#10068](https://github.com/penpot/penpot/issues/10068) (PR: [#10014](https://github.com/penpot/penpot/pull/10014))
|
||||
- Add configurable resource limits to ImageMagick image processing [#10223](https://github.com/penpot/penpot/issues/10223) (PR: [#10240](https://github.com/penpot/penpot/pull/10240))
|
||||
- Add resource limits to font processing child processes [#10234](https://github.com/penpot/penpot/issues/10234) (PR: [#10274](https://github.com/penpot/penpot/pull/10274))
|
||||
- Add color variants and positioning to selection size badge [#10258](https://github.com/penpot/penpot/issues/10258) (PR: [#9210](https://github.com/penpot/penpot/pull/9210))
|
||||
- Use hard reload for render engine switching in the workspace menu [#10441](https://github.com/penpot/penpot/issues/10441) (PR: [#10444](https://github.com/penpot/penpot/pull/10444))
|
||||
- Rotate size badge when shape is rotated [#10386](https://github.com/penpot/penpot/issues/10386) (PR: [#10393](https://github.com/penpot/penpot/pull/10393))
|
||||
- Add separate internal URI for exporter to handle Docker deployments where internal and public URIs differ [#10627](https://github.com/penpot/penpot/issues/10627) (PR: [#10630](https://github.com/penpot/penpot/pull/10630))
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix Plugin API variant creation failing due to undocumented multi-step workflow [#10075](https://github.com/penpot/penpot/issues/10075) (PR: [#10149](https://github.com/penpot/penpot/pull/10149))
|
||||
- Fix workspace crash when editing text shapes with degenerate selrect [#10617](https://github.com/penpot/penpot/issues/10617) (PR: [#10618](https://github.com/penpot/penpot/pull/10618))
|
||||
- Fix SVG stroke line join not applied when pasting strokes [#4836](https://github.com/penpot/penpot/issues/4836) (PR: [#9982](https://github.com/penpot/penpot/pull/9982), [#10019](https://github.com/penpot/penpot/pull/10019))
|
||||
- Fix blend-mode hover preview on canvas not reverted when dismissing dropdown (by @davidv399) [#9235](https://github.com/penpot/penpot/issues/9235) (PR: [#9237](https://github.com/penpot/penpot/pull/9237))
|
||||
- Fix View Mode mouse-leave and click in combination not working [#4855](https://github.com/penpot/penpot/issues/4855) (PR: [#9991](https://github.com/penpot/penpot/pull/9991))
|
||||
- Fix Storybook UI missing scrollbar (by @MilosM348) [#6049](https://github.com/penpot/penpot/issues/6049) (PR: [#9319](https://github.com/penpot/penpot/pull/9319))
|
||||
- Fix font selector missing intermediate font weights for Source Sans Pro and similar fonts (by @dhgoal) [#7378](https://github.com/penpot/penpot/issues/7378) (PR: [#9247](https://github.com/penpot/penpot/pull/9247))
|
||||
- Fix plugin API `typography.remove()` passing wrong parameter format (by @peter-rango) [#8223](https://github.com/penpot/penpot/issues/8223) (PR: [#9279](https://github.com/penpot/penpot/pull/9279))
|
||||
- Fix plugin API fills and strokes array elements being read-only (by @RenzoMXD) [#8357](https://github.com/penpot/penpot/issues/8357) (PR: [#9161](https://github.com/penpot/penpot/pull/9161))
|
||||
- Fix "Show Guides" shortcut not working on German keyboards (by @RenzoMXD) [#8423](https://github.com/penpot/penpot/issues/8423) (PR: [#9209](https://github.com/penpot/penpot/pull/9209))
|
||||
- Fix token validation failing when a malformed token exists in the Component category [#9010](https://github.com/penpot/penpot/issues/9010) (PR: [#9025](https://github.com/penpot/penpot/pull/9025), [#9825](https://github.com/penpot/penpot/pull/9825))
|
||||
- Fix Docker frontend image missing CSS reference (by @NativeTeachingAidsB) [#9135](https://github.com/penpot/penpot/issues/9135) (PR: [#9840](https://github.com/penpot/penpot/pull/9840))
|
||||
- Fix MCP media upload error and SVG data URI image parsing (by @claytonlin1110) [#9164](https://github.com/penpot/penpot/issues/9164) (PR: [#9201](https://github.com/penpot/penpot/pull/9201))
|
||||
- Fix lost-update race on team features during concurrent file creation (by @Lobster-0429) [#9197](https://github.com/penpot/penpot/issues/9197) (PR: [#9198](https://github.com/penpot/penpot/pull/9198))
|
||||
- Fix get-profile RPC method silently masking DB errors as "Anonymous User" (by @davidv399) [#9253](https://github.com/penpot/penpot/issues/9253) (PR: [#9254](https://github.com/penpot/penpot/pull/9254))
|
||||
- Fix crash when creating or editing tokens named "white" or "black" [#9256](https://github.com/penpot/penpot/issues/9256) (PR: [#9034](https://github.com/penpot/penpot/pull/9034))
|
||||
- Fix conditional use-ctx hook violation in shape-wrapper (by @Dexterity104) [#9280](https://github.com/penpot/penpot/issues/9280) (PR: [#9281](https://github.com/penpot/penpot/pull/9281))
|
||||
- Make ShapeImageIds byte conversion fallible to prevent panics (by @Dexterity104) [#9282](https://github.com/penpot/penpot/issues/9282) (PR: [#9283](https://github.com/penpot/penpot/pull/9283))
|
||||
- Fix plugin API showing incorrect error messages for invalid operations (by @bitcompass) [#9417](https://github.com/penpot/penpot/issues/9417) (PR: [#9486](https://github.com/penpot/penpot/pull/9486))
|
||||
- Add inactivity timeout to SSE sessions to match Streamable HTTP sessions [#9432](https://github.com/penpot/penpot/issues/9432) (PR: [#9464](https://github.com/penpot/penpot/pull/9464))
|
||||
- Fix component variant switching behaving differently on two identical copies (by @MischaPanch) [#9498](https://github.com/penpot/penpot/issues/9498) (PR: [#9434](https://github.com/penpot/penpot/pull/9434))
|
||||
- Add missing error message for invalid shadow token [#9583](https://github.com/penpot/penpot/issues/9583) (PR: [#9809](https://github.com/penpot/penpot/pull/9809))
|
||||
- Fix moving a component in a library triggering stale update notification in dependent files [#9629](https://github.com/penpot/penpot/issues/9629) (PR: [#9616](https://github.com/penpot/penpot/pull/9616))
|
||||
- Fix newly created token not visible when placed above existing tokens in the tree [#9711](https://github.com/penpot/penpot/issues/9711) (PR: [#9803](https://github.com/penpot/penpot/pull/9803))
|
||||
- Fix B(V) input label misalignment in HSB color picker [#9731](https://github.com/penpot/penpot/issues/9731) (PR: [#9793](https://github.com/penpot/penpot/pull/9793))
|
||||
- Fix text style name input appending font name instead of replacing it when edited [#9785](https://github.com/penpot/penpot/issues/9785) (PR: [#9784](https://github.com/penpot/penpot/pull/9784))
|
||||
- Fix shadow token creation not allowing empty blur or spread value [#9808](https://github.com/penpot/penpot/issues/9808) (PR: [#9809](https://github.com/penpot/penpot/pull/9809))
|
||||
- Fix thinner line in path when its stroke is deleted and added again [#9823](https://github.com/penpot/penpot/issues/9823) (PR: [#9836](https://github.com/penpot/penpot/pull/9836))
|
||||
- Fix layers panel perceivable lag when displaying changes [#9834](https://github.com/penpot/penpot/issues/9834)
|
||||
- Fix settings form visual layout broken after recent contribution [#9882](https://github.com/penpot/penpot/issues/9882) (PR: [#9883](https://github.com/penpot/penpot/pull/9883))
|
||||
- Fix crash when duplicating shapes with fill/stroke properties [#9893](https://github.com/penpot/penpot/issues/9893) (PR: [#9647](https://github.com/penpot/penpot/pull/9647))
|
||||
- Fix S3 storage failing with IRSA/Web Identity Token credentials (by @jpc2350) [#9927](https://github.com/penpot/penpot/issues/9927) (PR: [#9928](https://github.com/penpot/penpot/pull/9928))
|
||||
- Fix onboarding template spinner stuck after failed template download (by @jeffrey701) [#9931](https://github.com/penpot/penpot/issues/9931) (PR: [#9504](https://github.com/penpot/penpot/pull/9504))
|
||||
- Fix stroke caps not working correctly when there are other nodes in the middle of a path [#9987](https://github.com/penpot/penpot/issues/9987) (PR: [#9989](https://github.com/penpot/penpot/pull/9989))
|
||||
- Fix missing three dots button for column and row edit menu in WebKit/Safari [#9993](https://github.com/penpot/penpot/issues/9993) (PR: [#9994](https://github.com/penpot/penpot/pull/9994))
|
||||
- Fix exported path with strokes being cut off in SVG file [#9995](https://github.com/penpot/penpot/issues/9995) (PR: [#9996](https://github.com/penpot/penpot/pull/9996))
|
||||
- Fix French Canada locale falling back to French translations instead of French Canadian (by @alexismo) [#10017](https://github.com/penpot/penpot/issues/10017) (PR: [#10027](https://github.com/penpot/penpot/pull/10027))
|
||||
- Fix inconsistent float precision in typography lineHeight API responses [#3658](https://github.com/penpot/penpot/issues/3658) (PR: [#9973](https://github.com/penpot/penpot/pull/9973))
|
||||
- Fix variant switching preserving stale absolute positions in nested instance overrides [#9749](https://github.com/penpot/penpot/issues/9749) (PR: [#9691](https://github.com/penpot/penpot/pull/9691))
|
||||
- Fix missing top border on first element in shared libraries list [#9910](https://github.com/penpot/penpot/issues/9910) (PR: [#10062](https://github.com/penpot/penpot/pull/10062))
|
||||
- Fix theme not applied correctly in sample plugins [#9957](https://github.com/penpot/penpot/issues/9957) (PR: [#9955](https://github.com/penpot/penpot/pull/9955))
|
||||
- Fix export presets not being saved in view mode inspect panel [#9971](https://github.com/penpot/penpot/issues/9971) (PR: [#9972](https://github.com/penpot/penpot/pull/9972))
|
||||
- Fix cropped outer stroke of rotated board in view mode [#9978](https://github.com/penpot/penpot/issues/9978) (PR: [#9979](https://github.com/penpot/penpot/pull/9979))
|
||||
- Fix re-registration with same email showing verification prompt when email verification is disabled [#9998](https://github.com/penpot/penpot/issues/9998) (PR: [#9999](https://github.com/penpot/penpot/pull/9999))
|
||||
- Fix toggle color library visibility from color picker shortcut button [#10036](https://github.com/penpot/penpot/issues/10036) (PR: [#10129](https://github.com/penpot/penpot/pull/10129))
|
||||
- Improve image rendering performance by making snapshots async [#10045](https://github.com/penpot/penpot/issues/10045) (PR: [#10150](https://github.com/penpot/penpot/pull/10150))
|
||||
- Fix text layers wrapping to two lines after copy-pasting between files until edited [#10052](https://github.com/penpot/penpot/issues/10052) (PR: [#10081](https://github.com/penpot/penpot/pull/10081))
|
||||
- Fix plugin API creating shapes on wrong page and cross-page appendChild silently no-ops [#10078](https://github.com/penpot/penpot/issues/10078) (PR: [#10085](https://github.com/penpot/penpot/pull/10085))
|
||||
- Fix token dropdown triggered with select arrow not replacing previous content [#10104](https://github.com/penpot/penpot/issues/10104) (PR: [#10174](https://github.com/penpot/penpot/pull/10174))
|
||||
- Fix incorrect color count in color libraries dropdown (by @Krishcode264) [#10120](https://github.com/penpot/penpot/issues/10120) (PR: [#10281](https://github.com/penpot/penpot/pull/10281))
|
||||
- Fix color picker eyedropper returning wrong color values on some GPUs [#10135](https://github.com/penpot/penpot/issues/10135) (PR: [#10255](https://github.com/penpot/penpot/pull/10255))
|
||||
- Fix content from previous rendered frame visible in rounded frame transparent areas in viewer WebGL [#10161](https://github.com/penpot/penpot/issues/10161) (PR: [#10191](https://github.com/penpot/penpot/pull/10191))
|
||||
- Fix exported HTML/CSS not correctly rendered [#10214](https://github.com/penpot/penpot/issues/10214) (PR: [#10217](https://github.com/penpot/penpot/pull/10217))
|
||||
- Fix template import failing from dashboard when downloading example files (by @mdbenito) [#10317](https://github.com/penpot/penpot/issues/10317) (PR: [#10308](https://github.com/penpot/penpot/pull/10308))
|
||||
- Emit grid layout telemetry when adding via MCP or plugins [#10307](https://github.com/penpot/penpot/issues/10307) (PR: [#10319](https://github.com/penpot/penpot/pull/10319))
|
||||
- Fix guide hover feedback sticking or stopping after interaction [#10321](https://github.com/penpot/penpot/issues/10321) (PR: [#10333](https://github.com/penpot/penpot/pull/10333))
|
||||
- Fix rulers visible in thumbnail rendering [#10328](https://github.com/penpot/penpot/issues/10328)
|
||||
- Fix page switch blur disappearing early [#10332](https://github.com/penpot/penpot/issues/10332) (PR: [#10337](https://github.com/penpot/penpot/pull/10337))
|
||||
- Fix open overlay board mispositioned with repeated image artifacts in viewer WebGL [#10180](https://github.com/penpot/penpot/issues/10180) (PR: [#10191](https://github.com/penpot/penpot/pull/10191))
|
||||
- Fix color picker preview when browser has non-100% zoom level [#10265](https://github.com/penpot/penpot/issues/10265) (PR: [#10305](https://github.com/penpot/penpot/pull/10305))
|
||||
- Fix negative margins no longer working [#10001](https://github.com/penpot/penpot/issues/10001) (PR: [#10353](https://github.com/penpot/penpot/pull/10353))
|
||||
- Fix undo in properties panel reversing each drag step instead of the entire drag when adjusting numeric inputs [#10066](https://github.com/penpot/penpot/issues/10066) (PR: [#10193](https://github.com/penpot/penpot/pull/10193))
|
||||
- Fix Plugin API reference token creation and token application failing when the target token set is inactive [#10070](https://github.com/penpot/penpot/issues/10070), [#10071](https://github.com/penpot/penpot/issues/10071) (PR: [#10297](https://github.com/penpot/penpot/pull/10297))
|
||||
- Fix Plugin API rejecting numeric token values with a generic validation error [#10073](https://github.com/penpot/penpot/issues/10073) (PR: [#10270](https://github.com/penpot/penpot/pull/10270))
|
||||
- Fix Plugin API whole-page export failing with a generic HTTP error [#10076](https://github.com/penpot/penpot/issues/10076) (PR: [#10148](https://github.com/penpot/penpot/pull/10148))
|
||||
- Fix Plugin API applyToken failing on padding properties [#10077](https://github.com/penpot/penpot/issues/10077) (PR: [#10087](https://github.com/penpot/penpot/pull/10087))
|
||||
- Fix Plugin API crash when instancing or detaching variants [#10099](https://github.com/penpot/penpot/issues/10099) (PR: [#10140](https://github.com/penpot/penpot/pull/10140))
|
||||
- Fix black square appearing on first view mode launch [#10188](https://github.com/penpot/penpot/issues/10188) (PR: [#10210](https://github.com/penpot/penpot/pull/10210))
|
||||
- Fix Plugin API openPage() not switching active page within the same execution [#10195](https://github.com/penpot/penpot/issues/10195) (PR: [#10085](https://github.com/penpot/penpot/pull/10085))
|
||||
- Fix Plugin API appending shapes to the active page instead of the specified .root [#10196](https://github.com/penpot/penpot/issues/10196) (PR: [#10271](https://github.com/penpot/penpot/pull/10271))
|
||||
- Fix Plugin API createPage() not making the new page active synchronously [#10197](https://github.com/penpot/penpot/issues/10197) (PR: [#10085](https://github.com/penpot/penpot/pull/10085))
|
||||
- Fix Plugin API Flow.startingBoard returning null after createFlow [#10203](https://github.com/penpot/penpot/issues/10203) (PR: [#10244](https://github.com/penpot/penpot/pull/10244))
|
||||
- Fix Plugin API page.flows returning null instead of empty array when page has no flows [#10204](https://github.com/penpot/penpot/issues/10204) (PR: [#10246](https://github.com/penpot/penpot/pull/10246))
|
||||
- Fix Plugin API auto-creating a stray 'Flow 1' when wiring interactions [#10205](https://github.com/penpot/penpot/issues/10205) (PR: [#10231](https://github.com/penpot/penpot/pull/10231))
|
||||
- Fix Plugin API createText throwing error when called with an empty string [#10206](https://github.com/penpot/penpot/issues/10206) (PR: [#10219](https://github.com/penpot/penpot/pull/10219))
|
||||
- Fix export rendering auto-width text with a substituted fallback font [#10208](https://github.com/penpot/penpot/issues/10208) (PR: [#10238](https://github.com/penpot/penpot/pull/10238))
|
||||
- Expose 'Fix when scrolling' (sticky) constraint in the Plugin API [#10209](https://github.com/penpot/penpot/issues/10209) (PR: [#10218](https://github.com/penpot/penpot/pull/10218))
|
||||
- Fix long typography token name in remap modal [#10301](https://github.com/penpot/penpot/issues/10301) (PR: [#10356](https://github.com/penpot/penpot/pull/10356))
|
||||
- Fix 'Go to your Penpot' link on error page redirecting to the same team instead of user home [#10324](https://github.com/penpot/penpot/issues/10324) (PR: [#10322](https://github.com/penpot/penpot/pull/10322))
|
||||
- Fix premature WASM view-interaction end during pan/zoom pause [#10424](https://github.com/penpot/penpot/issues/10424) (PR: [#10425](https://github.com/penpot/penpot/pull/10425))
|
||||
- Fix emojis without fill but with outer stroke not being rendered [#10473](https://github.com/penpot/penpot/issues/10473) (PR: [#10519](https://github.com/penpot/penpot/pull/10519))
|
||||
- Fix texts with lots of emojis not being rendered across multiple tiles [#10474](https://github.com/penpot/penpot/issues/10474) (PR: [#10504](https://github.com/penpot/penpot/pull/10504))
|
||||
- Fix artifacts in texts with inner strokes [#10476](https://github.com/penpot/penpot/issues/10476) (PR: [#10509](https://github.com/penpot/penpot/pull/10509))
|
||||
- Fix open overlay position control showing only center icon instead of full grid [#10176](https://github.com/penpot/penpot/issues/10176) (PR: [#10512](https://github.com/penpot/penpot/pull/10512))
|
||||
- Fix wrong text color in selection size badge [#10256](https://github.com/penpot/penpot/issues/10256) (PR: [#10393](https://github.com/penpot/penpot/pull/10393))
|
||||
- Fix several issues with plugins [#10394](https://github.com/penpot/penpot/issues/10394)
|
||||
- Fix pixel grid rendered on top of the rulers [#10426](https://github.com/penpot/penpot/issues/10426) (PR: [#10430](https://github.com/penpot/penpot/pull/10430))
|
||||
- Fix double-clicking text without fills creating a black file [#10472](https://github.com/penpot/penpot/issues/10472) (PR: [#10483](https://github.com/penpot/penpot/pull/10483))
|
||||
- Fix SVG raw caching prevented by unnecessary Cow wrapping [#10488](https://github.com/penpot/penpot/issues/10488) (PR: [#10492](https://github.com/penpot/penpot/pull/10492))
|
||||
- Add component reset operation to plugin API [#10561](https://github.com/penpot/penpot/issues/10561) (PR: [#10533](https://github.com/penpot/penpot/pull/10533))
|
||||
- Fix blur menu alignment in Firefox [#10576](https://github.com/penpot/penpot/issues/10576) (PR: [#10575](https://github.com/penpot/penpot/pull/10575))
|
||||
- Fix sidebar not showing all elements with grid layout [#10539](https://github.com/penpot/penpot/issues/10539) (PR: [#10600](https://github.com/penpot/penpot/pull/10600))
|
||||
- Fix sidebar getting stuck when selecting shapes that haven't loaded yet [#10599](https://github.com/penpot/penpot/issues/10599) (PR: [#10600](https://github.com/penpot/penpot/pull/10600))
|
||||
- Fix text shape bounding boxes not updating after remote fonts finish loading [#10585](https://github.com/penpot/penpot/issues/10585) (PR: [#10566](https://github.com/penpot/penpot/pull/10566))
|
||||
- Fix text editor crash from Draft.js selection offset exceeding DOM node length [#10607](https://github.com/penpot/penpot/issues/10607) (PR: [#10608](https://github.com/penpot/penpot/pull/10608))
|
||||
- Fix workspace crash when converting SVG-raw shape to path [#10612](https://github.com/penpot/penpot/issues/10612) (PR: [#10613](https://github.com/penpot/penpot/pull/10613))
|
||||
- Fix component variant panel crash when selecting multiple copies with mismatched property counts [#10615](https://github.com/penpot/penpot/issues/10615) (PR: [#10616](https://github.com/penpot/penpot/pull/10616))
|
||||
- Fix workspace crash from recursion when clicking shape in comments mode [#10620](https://github.com/penpot/penpot/issues/10620) (PR: [#10622](https://github.com/penpot/penpot/pull/10622))
|
||||
- Fix Plugin API validation error when listing shared plugin data keys [#10628](https://github.com/penpot/penpot/issues/10628) (PR: [#10632](https://github.com/penpot/penpot/pull/10632))
|
||||
- Fix Plugin API silently dropping plugin data written to shared library [#10629](https://github.com/penpot/penpot/issues/10629) (PR: [#10632](https://github.com/penpot/penpot/pull/10632))
|
||||
- Fix workspace crash when event target is a DOM text node [#10640](https://github.com/penpot/penpot/issues/10640) (PR: [#10641](https://github.com/penpot/penpot/pull/10641))
|
||||
- Fix text shape position-data to include required fills in WASM and DOM calculation paths [#10646](https://github.com/penpot/penpot/issues/10646) (PR: [#10650](https://github.com/penpot/penpot/pull/10650))
|
||||
- Log expired OIDC tokens as auth failures instead of server errors [#10635](https://github.com/penpot/penpot/issues/10635) (PR: [#10636](https://github.com/penpot/penpot/pull/10636))
|
||||
- Return 400 instead of 500 when ImageMagick rejects invalid uploaded images [#10642](https://github.com/penpot/penpot/issues/10642) (PR: [#10643](https://github.com/penpot/penpot/pull/10643))
|
||||
|
||||
## 2.16.2
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix error 500 when submitting the contact form [#10178](https://github.com/penpot/penpot/issues/10178) (PR: [#10419](https://github.com/penpot/penpot/pull/10419))
|
||||
- Fix text editor modifying content and detaching applied typography tokens [#10389](https://github.com/penpot/penpot/issues/10389) (PR: [#10402](https://github.com/penpot/penpot/pull/10402))
|
||||
|
||||
## 2.16.1
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Batch multiple thumbnail deletions into a single RPC call [#9944](https://github.com/penpot/penpot/issues/9944) (PR: [#9943](https://github.com/penpot/penpot/pull/9943))
|
||||
- Add WebSocket proxy configuration for MCP in Nginx example (by @lancatlin) [#10153](https://github.com/penpot/penpot/issues/10153) (PR: [#10152](https://github.com/penpot/penpot/pull/10152))
|
||||
- Add tenant prefix to MCP Redis channel names for multi-environment isolation [#10277](https://github.com/penpot/penpot/issues/10277) (PR: [#10276](https://github.com/penpot/penpot/pull/10276))
|
||||
- Show MCP key on Integrations page and remove non-recoverable warning from modal [#10290](https://github.com/penpot/penpot/issues/10290) (PR: [#10298](https://github.com/penpot/penpot/pull/10298))
|
||||
- Add MCP status button with single-tab connection control [#9923](https://github.com/penpot/penpot/issues/9923) (PR: [#9930](https://github.com/penpot/penpot/pull/9930))
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix race condition between MCP initialization and plugin runtime [#10138](https://github.com/penpot/penpot/issues/10138) (PR: [#10137](https://github.com/penpot/penpot/pull/10137))
|
||||
- Filter ignorable React removeChild errors from browser extensions in error boundary [#10146](https://github.com/penpot/penpot/issues/10146) (PR: [#10145](https://github.com/penpot/penpot/pull/10145))
|
||||
- Show resolved values in font family token combobox when pasting comma-separated values [#10212](https://github.com/penpot/penpot/issues/10212) (PR: [#10215](https://github.com/penpot/penpot/pull/10215))
|
||||
- Fix MCP server status toggle persistence and missing workspace connection options [#10292](https://github.com/penpot/penpot/issues/10292) (PR: [#10226](https://github.com/penpot/penpot/pull/10226))
|
||||
- Allow pasting comma-separated emails in the invite members modal [#10173](https://github.com/penpot/penpot/issues/10173) (PR: [#10186](https://github.com/penpot/penpot/pull/10186))
|
||||
- Fix text element edit detaching applied color tokens [#9255](https://github.com/penpot/penpot/issues/9255) (PR: [#9525](https://github.com/penpot/penpot/pull/9525), [#9814](https://github.com/penpot/penpot/pull/9814), [#10340](https://github.com/penpot/penpot/pull/10340))
|
||||
|
||||
## 2.16.0
|
||||
## 2.16.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
- WebGL rendering (beta) user preference [#9683](https://github.com/penpot/penpot/issues/9683) (PR:[9113](https://github.com/penpot/penpot/pull/9113))
|
||||
- Design Tokens at the design tab: numeric fields with token selection in place [#9358](https://github.com/penpot/penpot/issues/9358)
|
||||
|
||||
@ -235,6 +25,7 @@
|
||||
- Duplicate token group [#9638](https://github.com/penpot/penpot/issues/9638) (PR: [#8886](https://github.com/penpot/penpot/pull/8886))
|
||||
- Copy token name from contextual menu [#9639](https://github.com/penpot/penpot/issues/9639) (PR: [#8566](https://github.com/penpot/penpot/pull/8566))
|
||||
- Add drag-to-change for numeric inputs in workspace sidebar (by @RenzoMXD) [#2466](https://github.com/penpot/penpot/issues/2466) (PR: [#8536](https://github.com/penpot/penpot/pull/8536))
|
||||
- Add CSS linter [#9636](https://github.com/penpot/penpot/issues/9636) (PR: [#8592](https://github.com/penpot/penpot/pull/8592))
|
||||
- Add per-group add button for typographies (by @eureka0928) [#5275](https://github.com/penpot/penpot/issues/5275) (PR: [#8895](https://github.com/penpot/penpot/pull/8895))
|
||||
- Add Find & Replace for text content and layer names (by @statxc) [#7108](https://github.com/penpot/penpot/issues/7108) (PR: [#8899](https://github.com/penpot/penpot/pull/8899), [#9687](https://github.com/penpot/penpot/pull/9687))
|
||||
- Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [#8773](https://github.com/penpot/penpot/issues/8773) (PR: [#8874](https://github.com/penpot/penpot/pull/8874))
|
||||
@ -265,13 +56,12 @@
|
||||
- Preserve Inkscape labels when pasting SVGs (by @jeffrey701) [#7869](https://github.com/penpot/penpot/issues/7869) (PR: [#9252](https://github.com/penpot/penpot/pull/9252))
|
||||
- Add Alt+click to expand layer subtree (by @MilosM348) [#7736](https://github.com/penpot/penpot/issues/7736) (PR: [#9179](https://github.com/penpot/penpot/pull/9179))
|
||||
- Allow deleting the profile avatar after uploading (by @moorsecopers99) [#9067](https://github.com/penpot/penpot/issues/9067) (PR: [#9068](https://github.com/penpot/penpot/pull/9068))
|
||||
- Clarify self-hosted OIDC configuration for containerized (by @sancfc) [#9764](https://github.com/penpot/penpot/issues/9764) (PR: [#9758](https://github.com/penpot/penpot/pull/9758))
|
||||
- Update User Guide with 2.16 features (by @myfunnyandy) [#9767](https://github.com/penpot/penpot/issues/9767) (PR: [#9768](https://github.com/penpot/penpot/pull/9768))
|
||||
- Improve file validation performance and fix orphan shape detection [#9790](https://github.com/penpot/penpot/issues/9790) (PR: [#9789](https://github.com/penpot/penpot/pull/9789))
|
||||
- Add v2.16 release notes (What's new modal) [#9945](https://github.com/penpot/penpot/issues/9945) (PR: [#9940](https://github.com/penpot/penpot/pull/9940))
|
||||
- Enable multi-instance horizontal scaling for MCP server [#10000](https://github.com/penpot/penpot/issues/10000) (PR: [#10013](https://github.com/penpot/penpot/pull/10013))
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix plugin API `Board.addRulerGuide` attaching guides to the page instead of the board due to a shadowed `id` binding; also correct the `'content:write'` permission error message and the `RulerGuideProxy` name (by @girafic) [#8225](https://github.com/penpot/penpot/issues/8225) (PR: [#8632](https://github.com/penpot/penpot/pull/8632))
|
||||
- Add Shift+Numpad aliases for zoom shortcuts (by @RenzoMXD) [#2457](https://github.com/penpot/penpot/issues/2457) (PR: [#9063](https://github.com/penpot/penpot/pull/9063))
|
||||
- Save and restore selection state in undo/redo (by @eureka0928) [#6007](https://github.com/penpot/penpot/issues/6007) (PR: [#8652](https://github.com/penpot/penpot/pull/8652))
|
||||
- Add guide locking and fix locked element selection in viewer (by @Dexterity104) [#8358](https://github.com/penpot/penpot/issues/8358) (PR: [#8949](https://github.com/penpot/penpot/pull/8949))
|
||||
@ -329,6 +119,7 @@
|
||||
- Fix `:heigth` typo in clipboard frame-same-size? (by @iot2edge) [#9249](https://github.com/penpot/penpot/issues/9249) (PR: [#9250](https://github.com/penpot/penpot/pull/9250))
|
||||
- Fix Settings Update button enabled state (by @moorsecopers99) [#9090](https://github.com/penpot/penpot/issues/9090) (PR: [#9091](https://github.com/penpot/penpot/pull/9091))
|
||||
- Fix library updates reappearing after reload [#9326](https://github.com/penpot/penpot/issues/9326) (PR: [#9563](https://github.com/penpot/penpot/pull/9563))
|
||||
- Fix dependency libraries visible after unlinking main library [#9331](https://github.com/penpot/penpot/issues/9331) (PR: [#9511](https://github.com/penpot/penpot/pull/9511))
|
||||
- Fix internal error on margins [#9309](https://github.com/penpot/penpot/issues/9309) (PR: [#9311](https://github.com/penpot/penpot/pull/9311))
|
||||
- Remove drag-to-change when token applied on numeric input [#9313](https://github.com/penpot/penpot/issues/9313) (PR: [#9314](https://github.com/penpot/penpot/pull/9314))
|
||||
- Fix extra input on canvas background [#9359](https://github.com/penpot/penpot/issues/9359) (PR: [#9360](https://github.com/penpot/penpot/pull/9360))
|
||||
@ -336,18 +127,20 @@
|
||||
- Fix several color picker issues [#9556](https://github.com/penpot/penpot/issues/9556) (PR: [#9558](https://github.com/penpot/penpot/pull/9558))
|
||||
- Fix asset icon broken on Asset tab [#9587](https://github.com/penpot/penpot/issues/9587) (PR: [#9612](https://github.com/penpot/penpot/pull/9612))
|
||||
- Fix text fill color stops updating in multiselect with texts [#9608](https://github.com/penpot/penpot/issues/9608) (PR: [#9549](https://github.com/penpot/penpot/pull/9549))
|
||||
- Fix editing a legacy text element silently detaches its color token [#9255](https://github.com/penpot/penpot/issues/9255) (PR: [#9525](https://github.com/penpot/penpot/pull/9525))
|
||||
- Fix token application to grid paddings [#9494](https://github.com/penpot/penpot/issues/9494) (PR: [#9630](https://github.com/penpot/penpot/pull/9630))
|
||||
- Fix file crashing when switching a variant [#9259](https://github.com/penpot/penpot/issues/9259) (PR: [#9147](https://github.com/penpot/penpot/pull/9147))
|
||||
- Fix set activation after renaming [#9329](https://github.com/penpot/penpot/issues/9329) (PR: [#9545](https://github.com/penpot/penpot/pull/9545))
|
||||
- Fix font selection position hiding available fonts [#9489](https://github.com/penpot/penpot/issues/9489) (PR: [#9499](https://github.com/penpot/penpot/pull/9499))
|
||||
- Fix numeric input changes not saved when clicking on viewport [#9491](https://github.com/penpot/penpot/issues/9491) (PR: [#9548](https://github.com/penpot/penpot/pull/9548))
|
||||
- Fix resize cursor appearing on login and register buttons [#9505](https://github.com/penpot/penpot/issues/9505) (PR: [#9590](https://github.com/penpot/penpot/pull/9590))
|
||||
- Fix version restore restoring first previewed version instead of selected one [#9588](https://github.com/penpot/penpot/issues/9588) (PR: [#9626](https://github.com/penpot/penpot/pull/9626))
|
||||
- Fix incorrect error message when applying tokens while editing text [#9620](https://github.com/penpot/penpot/issues/9620) (PR: [#9708](https://github.com/penpot/penpot/pull/9708))
|
||||
- Fix standalone tokens ordering separated from token groups [#9733](https://github.com/penpot/penpot/issues/9733) (PR: [#9736](https://github.com/penpot/penpot/pull/9736))
|
||||
- Fix delete invitation modal readability in light theme [#9737](https://github.com/penpot/penpot/issues/9737) (PR: [#9747](https://github.com/penpot/penpot/pull/9747))
|
||||
- Fix team invitation not automatically accepted after account validation [#9776](https://github.com/penpot/penpot/issues/9776) (PR: [#9782](https://github.com/penpot/penpot/pull/9782))
|
||||
- Fix design tokens vanishing from the sidebar when a token name collides with a token-group prefix from another active set (e.g. `a` in one set and `a.b` in another); the colliding token is now kept and rendered as a broken pill [Github #9584](https://github.com/penpot/penpot/issues/9584)
|
||||
- Fix Plugin API addRulerGuide creating guides on page instead of board (by @girafic) [#8225](https://github.com/penpot/penpot/issues/8225) (PR: [#8632](https://github.com/penpot/penpot/pull/8632))
|
||||
- Fix text editor not swapping correctly when enabling/disabling WebGL [#10015](https://github.com/penpot/penpot/issues/10015)
|
||||
- Fix WebGL renderer focus mode leaving artefacts [#10061](https://github.com/penpot/penpot/issues/10061) (PR: [#10091](https://github.com/penpot/penpot/pull/10091))
|
||||
- Fix double click on text selecting underlying element when WebGL render is enabled [#10080](https://github.com/penpot/penpot/issues/10080) (PR: [#10123](https://github.com/penpot/penpot/pull/10123))
|
||||
- Fix publishing or unpublishing file as library failing with unexpected state found error [#10094](https://github.com/penpot/penpot/issues/10094) (PR: [#10093](https://github.com/penpot/penpot/pull/10093))
|
||||
- Fix team invitation failing when email address contains consecutive dots in domain [#10097](https://github.com/penpot/penpot/issues/10097) (PR: [#10096](https://github.com/penpot/penpot/pull/10096))
|
||||
- Add detailed error messages for unspecified import errors [#9759](https://github.com/penpot/penpot/issues/9759) (PR: [#9886](https://github.com/penpot/penpot/pull/9886))
|
||||
- Fix mask with children not applying blur correctly [#10004](https://github.com/penpot/penpot/issues/10004) (PR: [#10028](https://github.com/penpot/penpot/pull/10028))
|
||||
- Fix WebGL italic font mismatch when custom font variants are mapped [#10060](https://github.com/penpot/penpot/issues/10060) (PR: [#10122](https://github.com/penpot/penpot/pull/10122))
|
||||
|
||||
|
||||
## 2.15.4
|
||||
@ -382,6 +175,7 @@
|
||||
|
||||
- Fix mcp related internal config for docker images [GH #9565](https://github.com/penpot/penpot/pull/9565)
|
||||
|
||||
|
||||
## 2.15.1
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
@ -392,6 +186,7 @@
|
||||
|
||||
- Fix "Help & Learning" submenu vertical alignment in account menu (by @juan-flores077) [#9137](https://github.com/penpot/penpot/issues/9137) (PR: [#9138](https://github.com/penpot/penpot/pull/9138))
|
||||
|
||||
|
||||
## 2.15.0
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
128
CONTRIBUTING.md
128
CONTRIBUTING.md
@ -14,9 +14,9 @@ Center](https://help.penpot.app/).
|
||||
- [Reporting Bugs](#reporting-bugs)
|
||||
- [Pull Requests](#pull-requests)
|
||||
- [Workflow](#workflow)
|
||||
- [Format](#format)
|
||||
- [Title format](#title-format)
|
||||
- [Description](#description)
|
||||
- [Branch naming](#branch-naming)
|
||||
- [Review process](#review-process)
|
||||
- [What we won't accept](#what-we-wont-accept)
|
||||
- [Good first issues](#good-first-issues)
|
||||
@ -73,23 +73,35 @@ Advisories](https://github.com/penpot/penpot/security/advisories)
|
||||
4. **Format and lint** — run the checks described in
|
||||
[Formatting and Linting](#formatting-and-linting) before submitting.
|
||||
|
||||
### Format
|
||||
|
||||
#### Title
|
||||
|
||||
> **IMPORTANT:** When a PR is squash-merged, the PR title becomes the
|
||||
> commit message on the main branch. Getting the title right matters.
|
||||
### Title format
|
||||
|
||||
Pull request titles **must** follow the same convention as commit subjects:
|
||||
|
||||
```
|
||||
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
|
||||
:emoji: <subject>
|
||||
```
|
||||
|
||||
Read [Creating Commits](./.serena/memories/workflow/creating-commits.md)
|
||||
for more concrete information.
|
||||
- Use the **imperative mood** (e.g. "Fix", not "Fixed").
|
||||
- Capitalize the first letter of the subject.
|
||||
- Do not end the subject with a period.
|
||||
- Keep the subject to **70 characters** or fewer.
|
||||
- Use one of the [commit type emojis](#commit-types) listed below.
|
||||
|
||||
#### Description
|
||||
When a PR contains multiple unrelated commits, choose the emoji that
|
||||
best represents the dominant change.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```
|
||||
:bug: Fix unexpected error on launching modal
|
||||
:sparkles: Enable new modal for profile
|
||||
:zap: Improve performance of dashboard navigation
|
||||
```
|
||||
|
||||
> **Note:** When a PR is squash-merged, the PR title becomes the
|
||||
> commit message on the main branch. Getting the title right matters.
|
||||
|
||||
### Description
|
||||
|
||||
Every pull request should include a description that helps reviewers
|
||||
understand the change quickly:
|
||||
@ -102,8 +114,24 @@ understand the change quickly:
|
||||
5. **Breaking changes** — call out anything that affects existing users
|
||||
or requires migration steps.
|
||||
|
||||
Read [Creating PRs](./.serena/memories/workflow/creating-prs.md)
|
||||
for more concrete information.
|
||||
### Branch naming
|
||||
|
||||
Use a descriptive branch name that reflects the type and scope of the
|
||||
change:
|
||||
|
||||
```
|
||||
<type>/<short-description>
|
||||
```
|
||||
|
||||
Types: `fix`, `feat`, `refactor`, `docs`, `chore`, `perf`.
|
||||
|
||||
Optionally include the issue number:
|
||||
|
||||
```
|
||||
fix/9122-email-blacklisting
|
||||
feat/export-webp
|
||||
refactor/layout-sizing
|
||||
```
|
||||
|
||||
### Review process
|
||||
|
||||
@ -123,15 +151,15 @@ for more concrete information.
|
||||
To save time on both sides, please avoid submitting PRs that:
|
||||
|
||||
- Introduce new dependencies without prior discussion.
|
||||
- Change the build system or CI configuration without maintainer approval.
|
||||
- Mix unrelated changes in a single PR — keep PRs focused on one concern.
|
||||
- Submit AI-generated code without human review.
|
||||
- Skip local syntax and formatting checks before submitting.
|
||||
- Change the build system or CI configuration without maintainer
|
||||
approval.
|
||||
- Mix unrelated changes in a single PR — keep PRs focused on one
|
||||
concern.
|
||||
- Skip the [discussion step](#workflow) for non-bug-fix changes.
|
||||
|
||||
### Good first issues
|
||||
|
||||
We use the `good first issue` label to mark issues appropriate for newcomers.
|
||||
We use the `easy fix` label to mark issues appropriate for newcomers.
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
@ -147,26 +175,26 @@ Commit messages must follow this format:
|
||||
|
||||
### Commit types
|
||||
|
||||
| Emoji | Code | Description |
|
||||
| ---------------------- | ------------------------ | -------------------------- |
|
||||
| :bug: | `:bug:` | Bug fix |
|
||||
| :sparkles: | `:sparkles:` | Improvement or enhancement |
|
||||
| :tada: | `:tada:` | New feature |
|
||||
| :recycle: | `:recycle:` | Refactor |
|
||||
| :lipstick: | `:lipstick:` | Cosmetic changes |
|
||||
| :ambulance: | `:ambulance:` | Critical bug fix |
|
||||
| :books: | `:books:` | Documentation |
|
||||
| :construction: | `:construction:` | Work in progress |
|
||||
| :boom: | `:boom:` | Breaking change |
|
||||
| :wrench: | `:wrench:` | Configuration update |
|
||||
| :zap: | `:zap:` | Performance improvement |
|
||||
| :whale: | `:whale:` | Docker-related change |
|
||||
| :paperclip: | `:paperclip:` | Other non-relevant changes |
|
||||
| :arrow_up: | `:arrow_up:` | Dependency update |
|
||||
| :arrow_down: | `:arrow_down:` | Dependency downgrade |
|
||||
| :fire: | `:fire:` | Removal of code or files |
|
||||
| :globe_with_meridians: | `:globe_with_meridians:` | Add or update translations |
|
||||
| :rocket: | `:rocket:` | Epic or highlight |
|
||||
| Emoji | Description |
|
||||
|-------|-------------|
|
||||
| :bug: | Bug fix |
|
||||
| :sparkles: | Improvement or enhancement |
|
||||
| :tada: | New feature |
|
||||
| :recycle: | Refactor |
|
||||
| :lipstick: | Cosmetic changes |
|
||||
| :ambulance: | Critical bug fix |
|
||||
| :books: | Documentation |
|
||||
| :construction: | Work in progress |
|
||||
| :boom: | Breaking change |
|
||||
| :wrench: | Configuration update |
|
||||
| :zap: | Performance improvement |
|
||||
| :whale: | Docker-related change |
|
||||
| :paperclip: | Other non-relevant changes |
|
||||
| :arrow_up: | Dependency update |
|
||||
| :arrow_down: | Dependency downgrade |
|
||||
| :fire: | Removal of code or files |
|
||||
| :globe_with_meridians: | Add or update translations |
|
||||
| :rocket: | Epic or highlight |
|
||||
|
||||
### Rules
|
||||
|
||||
@ -189,13 +217,29 @@ Commit messages must follow this format:
|
||||
|
||||
## Formatting and Linting
|
||||
|
||||
Each module has its own linting and formatting commands — see the relevant one on the
|
||||
[Serena Memories](./.serena/memories/)
|
||||
We use [cljfmt](https://github.com/weavejester/cljfmt) for formatting and
|
||||
[clj-kondo](https://github.com/clj-kondo/clj-kondo) for linting.
|
||||
|
||||
```bash
|
||||
# Check formatting (does not modify files)
|
||||
./scripts/check-fmt
|
||||
|
||||
# Fix formatting (modifies files in place)
|
||||
./scripts/fmt
|
||||
|
||||
# Lint
|
||||
./scripts/lint
|
||||
```
|
||||
|
||||
Ideally, run these as git pre-commit hooks.
|
||||
[Husky](https://typicode.github.io/husky/#/) is a convenient option for
|
||||
setting this up.
|
||||
|
||||
## Changelog
|
||||
|
||||
The changelog is updated automatically as part of the release process. Contributors
|
||||
should **not** modify `CHANGES.md` manually in their pull requests.
|
||||
When your change is user-facing or otherwise notable, add an entry to
|
||||
[CHANGES.md](CHANGES.md) following the same commit-type conventions. Reference
|
||||
the relevant GitHub issue or Taiga user story.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.digitalpublicgoods.net/r/penpot" rel="nofollow">
|
||||
<img alt="Verified DPG" src="https://img.shields.io/badge/Verified-DPG-3333AB?logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iMzEiIGhlaWdodD0iMzMiIHZpZXdCb3g9IjAgMCAzMSAzMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0LjIwMDggMjEuMzY3OEwxMC4xNzM2IDE4LjAxMjRMMTEuNTIxOSAxNi40MDAzTDEzLjk5MjggMTguNDU5TDE5LjYyNjkgMTIuMjExMUwyMS4xOTA5IDEzLjYxNkwxNC4yMDA4IDIxLjM2NzhaTTI0LjYyNDEgOS4zNTEyN0wyNC44MDcxIDMuMDcyOTdMMTguODgxIDUuMTg2NjJMMTUuMzMxNCAtMi4zMzA4MmUtMDVMMTEuNzgyMSA1LjE4NjYyTDUuODU2MDEgMy4wNzI5N0w2LjAzOTA2IDkuMzUxMjdMMCAxMS4xMTc3TDMuODQ1MjEgMTYuMDg5NUwwIDIxLjA2MTJMNi4wMzkwNiAyMi44Mjc3TDUuODU2MDEgMjkuMTA2TDExLjc4MjEgMjYuOTkyM0wxNS4zMzE0IDMyLjE3OUwxOC44ODEgMjYuOTkyM0wyNC44MDcxIDI5LjEwNkwyNC42MjQxIDIyLjgyNzdMMzAuNjYzMSAyMS4wNjEyTDI2LjgxNzYgMTYuMDg5NUwzMC42NjMxIDExLjExNzdMMjQuNjI0MSA5LjM1MTI3WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==">
|
||||
<img alt="Verified DPG" src="https://img.shields.io/badge/Verified-DPG-blue.svg">
|
||||
</a>
|
||||
<a href="https://community.penpot.app" rel="nofollow">
|
||||
<img alt="Penpot Community" src="https://img.shields.io/discourse/posts?server=https%3A%2F%2Fcommunity.penpot.app">
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
We take the security of this project seriously. If you have discovered
|
||||
a security vulnerability, please do **not** open a public issue.
|
||||
|
||||
Please report vulnerabilities through the [GitHub Security Advisories](https://github.com/penpot/penpot/security/advisories
|
||||
) feature in the Penpot repository.
|
||||
Please report vulnerabilities via email to: **[support@penpot.app]**
|
||||
|
||||
|
||||
### What to include:
|
||||
|
||||
|
||||
@ -7,9 +7,6 @@ list.
|
||||
|
||||
## Security
|
||||
|
||||
* Noob Researcher
|
||||
* Kirubakaran N
|
||||
* Alisher (@7megaumka7)
|
||||
* Husnain Iqbal (CEO OF ALPHA INFERNO PVT LTD)
|
||||
* [Shiraz Ali Khan](https://www.linkedin.com/in/shiraz-ali-khan-1ba508180/)
|
||||
* Vaibhav Shukla
|
||||
|
||||
262
backend/AGENTS.md
Normal file
262
backend/AGENTS.md
Normal file
@ -0,0 +1,262 @@
|
||||
# Penpot Backend – Agent Instructions
|
||||
|
||||
Clojure backend (RPC) service running on the JVM.
|
||||
|
||||
Uses Integrant for dependency injection, PostgreSQL for storage, and
|
||||
Redis for messaging/caching.
|
||||
|
||||
## General Guidelines
|
||||
|
||||
To ensure consistency across the Penpot JVM stack, all contributions must adhere
|
||||
to these criteria.
|
||||
|
||||
IMPORTANT: all CLI commands should be executed under backend/
|
||||
subdirectory for make them work correctly.
|
||||
|
||||
### 1. Testing & Validation
|
||||
|
||||
* **Coverage:** If code is added or modified in `src/`, corresponding
|
||||
tests in `test/backend_tests/` must be added or updated.
|
||||
|
||||
* **Execution:**
|
||||
* **Isolated:** Run `clojure -M:dev:test --focus backend-tests.my-ns-test` for the specific test namespace.
|
||||
* **Regression:** Run `clojure -M:dev:test` to ensure the suite passes without regressions in related functional areas.
|
||||
|
||||
### 2. Code Quality & Formatting
|
||||
|
||||
* **Linting:** All code must pass linter checks (run `pnpm run lint:clj` or `pnpm run lint` on the repository root)
|
||||
* **Formatting:** All the code must pass the formatting check (run `pnpm run
|
||||
check-fmt`). Use `pnpm run fmt` to fix formatting issues. Avoid "dirty"
|
||||
diffs caused by unrelated whitespace changes.
|
||||
* **Type Hinting:** Use explicit JVM type hints (e.g., `^String`, `^long`) in
|
||||
performance-critical paths to avoid reflection overhead.
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Namespace Overview
|
||||
|
||||
The source is located under `src` directory and this is a general overview of
|
||||
namespaces structure:
|
||||
|
||||
- `app.rpc.commands.*` – RPC command implementations (`auth`, `files`, `teams`, etc.)
|
||||
- `app.http.*` – HTTP routes and middleware
|
||||
- `app.db.*` – Database layer
|
||||
- `app.tasks.*` – Background job tasks
|
||||
- `app.main` – Integrant system setup and entrypoint
|
||||
- `app.loggers` – Internal loggers (auditlog, mattermost, etc.) (not to be confused with `app.common.logging`)
|
||||
|
||||
### RPC
|
||||
|
||||
The RPC methods are implemented using a multimethod-like structure via the
|
||||
`app.util.services` namespace. The main RPC methods are collected under
|
||||
`app.rpc.commands` namespace and exposed under `/api/rpc/command/<cmd-name>`.
|
||||
|
||||
The RPC method accepts POST and GET requests indistinctly and uses the `Accept`
|
||||
header to negotiate the response encoding (which can be Transit — the default —
|
||||
or plain JSON). It also accepts Transit (default) or JSON as input, which should
|
||||
be indicated using the `Content-Type` header.
|
||||
|
||||
The main convention is: use `get-` prefix on RPC name when we want READ
|
||||
operation.
|
||||
|
||||
Example of RPC method definition:
|
||||
|
||||
```clojure
|
||||
(sv/defmethod ::my-command
|
||||
{::rpc/auth true ;; requires auth
|
||||
::doc/added "1.18"
|
||||
::sm/params [:map ...] ;; malli input schema
|
||||
::sm/result [:map ...]} ;; malli output schema
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
;; return a plain map or throw
|
||||
{:id (uuid/next)})
|
||||
```
|
||||
|
||||
Look under `src/app/rpc/commands/*.clj` to see more examples.
|
||||
|
||||
### Tests
|
||||
|
||||
Test namespaces match `.*-test$` under `test/`. Config is in `tests.edn`.
|
||||
|
||||
|
||||
### Integrant System
|
||||
|
||||
The `src/app/main.clj` declares the system map. Each key is a component; values
|
||||
are config maps with `::ig/ref` for dependencies. Components implement
|
||||
`ig/init-key` / `ig/halt-key!`.
|
||||
|
||||
|
||||
### Connecting to the Database
|
||||
|
||||
Two PostgreSQL databases are used in this environment:
|
||||
|
||||
| Database | Purpose | Connection string |
|
||||
|---------------|--------------------|----------------------------------------------------|
|
||||
| `penpot` | Development / app | `postgresql://penpot:penpot@postgres/penpot` |
|
||||
| `penpot_test` | Test suite | `postgresql://penpot:penpot@postgres/penpot_test` |
|
||||
|
||||
**Interactive psql session:**
|
||||
|
||||
```bash
|
||||
# development DB
|
||||
psql "postgresql://penpot:penpot@postgres/penpot"
|
||||
|
||||
# test DB
|
||||
psql "postgresql://penpot:penpot@postgres/penpot_test"
|
||||
```
|
||||
|
||||
**One-shot query (non-interactive):**
|
||||
|
||||
```bash
|
||||
psql "postgresql://penpot:penpot@postgres/penpot" -c "SELECT id, name FROM team LIMIT 5;"
|
||||
```
|
||||
|
||||
**Useful psql meta-commands:**
|
||||
|
||||
```
|
||||
\dt -- list all tables
|
||||
\d <table> -- describe a table (columns, types, constraints)
|
||||
\di -- list indexes
|
||||
\q -- quit
|
||||
```
|
||||
|
||||
> **Migrations table:** Applied migrations are tracked in the `migrations` table
|
||||
> with columns `module`, `step`, and `created_at`. When renaming a migration
|
||||
> logical name, update this table in both databases to match the new name;
|
||||
> otherwise the runner will attempt to re-apply the migration on next startup.
|
||||
|
||||
```bash
|
||||
# Example: fix a renamed migration entry in the test DB
|
||||
psql "postgresql://penpot:penpot@postgres/penpot_test" \
|
||||
-c "UPDATE migrations SET step = 'new-name' WHERE step = 'old-name';"
|
||||
```
|
||||
|
||||
### Database Access (Clojure)
|
||||
|
||||
`app.db` wraps next.jdbc. Queries use a SQL builder that auto-converts kebab-case ↔ snake_case.
|
||||
|
||||
```clojure
|
||||
;; Query helpers
|
||||
(db/get cfg-or-pool :table {:id id}) ; fetch one row (throws if missing)
|
||||
(db/get* cfg-or-pool :table {:id id}) ; fetch one row (returns nil)
|
||||
(db/query cfg-or-pool :table {:team-id team-id}) ; fetch multiple rows
|
||||
(db/insert! cfg-or-pool :table {:name "x" :team-id id}) ; insert
|
||||
(db/update! cfg-or-pool :table {:name "y"} {:id id}) ; update
|
||||
(db/delete! cfg-or-pool :table {:id id}) ; delete
|
||||
|
||||
;; Run multiple statements/queries on single connection
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(db/insert! conn :table row1)
|
||||
(db/insert! conn :table row2))
|
||||
|
||||
|
||||
;; Transactions
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
(db/insert! conn :table row)))
|
||||
```
|
||||
|
||||
Almost all methods in the `app.db` namespace accept `pool`, `conn`, or
|
||||
`cfg` as params.
|
||||
|
||||
Migrations live in `src/app/migrations/` as numbered SQL files. They run automatically on startup.
|
||||
|
||||
|
||||
### Error Handling
|
||||
|
||||
The exception helpers are defined on Common module, and are available under
|
||||
`app.common.exceptions` namespace.
|
||||
|
||||
Example of raising an exception:
|
||||
|
||||
```clojure
|
||||
(ex/raise :type :not-found
|
||||
:code :object-not-found
|
||||
:hint "File does not exist"
|
||||
:file-id id)
|
||||
```
|
||||
|
||||
Common types: `:not-found`, `:validation`, `:authorization`, `:conflict`, `:internal`.
|
||||
|
||||
|
||||
### Performance Macros (`app.common.data.macros`)
|
||||
|
||||
Always prefer these macros over their `clojure.core` equivalents — they provide
|
||||
optimized implementations:
|
||||
|
||||
```clojure
|
||||
(dm/select-keys m [:a :b]) ;; faster than core/select-keys
|
||||
(dm/get-in obj [:a :b :c]) ;; faster than core/get-in
|
||||
(dm/str "a" "b" "c") ;; string concatenation
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
`src/app/config.clj` reads `PENPOT_*` environment variables, validated with
|
||||
Malli. Access anywhere via `(cf/get :smtp-host)`. Feature flags: `(cf/flags
|
||||
:enable-smtp)`.
|
||||
|
||||
|
||||
### Background Tasks
|
||||
|
||||
Background tasks live in `src/app/tasks/`. Each task is an Integrant component
|
||||
that exposes a `::handler` key and follows this three-method pattern:
|
||||
|
||||
```clojure
|
||||
(defmethod ig/assert-key ::handler ;; validate config at startup
|
||||
[_ params]
|
||||
(assert (db/pool? (::db/pool params)) "expected a valid database pool"))
|
||||
|
||||
(defmethod ig/expand-key ::handler ;; inject defaults before init
|
||||
[k v]
|
||||
{k (assoc v ::my-option default-value)})
|
||||
|
||||
(defmethod ig/init-key ::handler ;; return the task fn
|
||||
[_ cfg]
|
||||
(fn [_task] ;; receives the task row from the worker
|
||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
||||
;; … do work …
|
||||
))))
|
||||
```
|
||||
|
||||
**Wiring a new task** requires two changes in `src/app/main.clj`:
|
||||
|
||||
1. **Handler config** – add an entry in `system-config` with the dependencies:
|
||||
|
||||
```clojure
|
||||
:app.tasks.my-task/handler
|
||||
{::db/pool (ig/ref ::db/pool)}
|
||||
```
|
||||
|
||||
2. **Registry + cron** – register the handler name and schedule it:
|
||||
|
||||
```clojure
|
||||
;; in ::wrk/registry ::wrk/tasks map:
|
||||
:my-task (ig/ref :app.tasks.my-task/handler)
|
||||
|
||||
;; in worker-config ::wrk/cron ::wrk/entries vector:
|
||||
{:cron #penpot/cron "0 0 0 * * ?" ;; daily at midnight
|
||||
:task :my-task}
|
||||
```
|
||||
|
||||
**Useful cron patterns** (Quartz format — six fields: s m h dom mon dow):
|
||||
|
||||
| Expression | Meaning |
|
||||
|------------------------------|--------------------|
|
||||
| `"0 0 0 * * ?"` | Daily at midnight |
|
||||
| `"0 0 */6 * * ?"` | Every 6 hours |
|
||||
| `"0 */5 * * * ?"` | Every 5 minutes |
|
||||
|
||||
**Time helpers** (`app.common.time`):
|
||||
|
||||
```clojure
|
||||
(ct/now) ;; current instant
|
||||
(ct/duration {:hours 1}) ;; java.time.Duration
|
||||
(ct/minus (ct/now) some-duration) ;; subtract duration from instant
|
||||
```
|
||||
|
||||
`db/interval` converts a `Duration` (or millis / string) to a PostgreSQL
|
||||
interval object suitable for use in SQL queries:
|
||||
|
||||
```clojure
|
||||
(db/interval (ct/duration {:hours 1})) ;; → PGInterval "3600.0 seconds"
|
||||
```
|
||||
@ -4,9 +4,9 @@
|
||||
:deps
|
||||
{penpot/common {:local/root "../common"}
|
||||
org.clojure/clojure {:mvn/version "1.12.5"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.1"}
|
||||
org.clojure/tools.namespace {:mvn/version "1.5.0"}
|
||||
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-12"}
|
||||
com.github.luben/zstd-jni {:mvn/version "1.5.7-4"}
|
||||
|
||||
io.prometheus/simpleclient {:mvn/version "0.16.0"}
|
||||
io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"}
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"}
|
||||
|
||||
io.lettuce/lettuce-core {:mvn/version "7.6.0.RELEASE"}
|
||||
io.lettuce/lettuce-core {:mvn/version "7.5.1.RELEASE"}
|
||||
;; Minimal dependencies required by lettuce, we need to include them
|
||||
;; explicitly because clojure dependency management does not support
|
||||
;; yet the BOM format.
|
||||
@ -25,7 +25,7 @@
|
||||
io.micrometer/micrometer-observation {:mvn/version "1.14.2"}
|
||||
|
||||
java-http-clj/java-http-clj {:mvn/version "0.4.3"}
|
||||
com.google.guava/guava {:mvn/version "33.6.0-jre"}
|
||||
com.google.guava/guava {:mvn/version "33.4.8-jre"}
|
||||
|
||||
funcool/yetti
|
||||
{:git/tag "v11.10"
|
||||
@ -34,15 +34,15 @@
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
com.github.seancorfield/next.jdbc
|
||||
{:mvn/version "1.3.1118"}
|
||||
{:mvn/version "1.3.1093"}
|
||||
|
||||
metosin/reitit-core {:mvn/version "0.10.1"}
|
||||
metosin/reitit-core {:mvn/version "0.9.1"}
|
||||
nrepl/nrepl {:mvn/version "1.7.0"}
|
||||
|
||||
org.postgresql/postgresql {:mvn/version "42.7.13"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.53.2.1"}
|
||||
org.postgresql/postgresql {:mvn/version "42.7.11"}
|
||||
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
|
||||
|
||||
com.zaxxer/HikariCP {:mvn/version "7.1.0"}
|
||||
com.zaxxer/HikariCP {:mvn/version "7.0.2"}
|
||||
|
||||
io.whitfin/siphash {:mvn/version "2.0.0"}
|
||||
|
||||
@ -51,28 +51,29 @@
|
||||
|
||||
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.4"}
|
||||
|
||||
org.jsoup/jsoup {:mvn/version "1.23.1"}
|
||||
org.jsoup/jsoup {:mvn/version "1.21.2"}
|
||||
org.im4java/im4java
|
||||
{:git/tag "1.4.0-penpot-2"
|
||||
:git/sha "e2b3e16"
|
||||
:git/url "https://github.com/penpot/im4java"}
|
||||
|
||||
at.yawk.lz4/lz4-java
|
||||
{:mvn/version "1.11.1"}
|
||||
{:mvn/version "1.11.0"}
|
||||
|
||||
org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"}
|
||||
|
||||
dawran6/emoji {:mvn/version "0.2.0"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.12.8"}
|
||||
markdown-clj/markdown-clj {:mvn/version "1.12.4"}
|
||||
|
||||
;; Pretty Print specs
|
||||
pretty-spec/pretty-spec {:mvn/version "0.1.4"}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.50.1"}
|
||||
software.amazon.awssdk/sts {:mvn/version "2.50.1"}}
|
||||
software.amazon.awssdk/s3 {:mvn/version "2.44.4"}}
|
||||
|
||||
:paths ["src" "resources" "target/classes"]
|
||||
:aliases
|
||||
{:dev
|
||||
{:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
:extra-deps
|
||||
{com.bhauman/rebel-readline {:mvn/version "0.1.11"}
|
||||
{:extra-deps
|
||||
{com.bhauman/rebel-readline {:mvn/version "0.1.5"}
|
||||
clojure-humanize/clojure-humanize {:mvn/version "0.2.2"}
|
||||
org.clojure/data.csv {:mvn/version "1.1.1"}
|
||||
com.clojure-goes-fast/clj-async-profiler {:mvn/version "2.0.0-beta1"}
|
||||
@ -81,12 +82,14 @@
|
||||
|
||||
:build
|
||||
{:extra-deps
|
||||
{io.github.clojure/tools.build {:mvn/version "0.10.14"}}
|
||||
{io.github.clojure/tools.build {:mvn/version "0.10.10"}}
|
||||
:ns-default build}
|
||||
|
||||
:test
|
||||
{:main-opts ["-m" "kaocha.runner"]
|
||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
|
||||
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"
|
||||
"--sun-misc-unsafe-memory-access=allow"
|
||||
"--enable-native-access=ALL-UNNAMED"]
|
||||
:extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}}
|
||||
|
||||
:outdated
|
||||
|
||||
@ -104,20 +104,24 @@
|
||||
[]
|
||||
(try
|
||||
(main/start)
|
||||
:started
|
||||
(catch Throwable cause
|
||||
(ex/print-throwable cause))))
|
||||
|
||||
(defn- stop
|
||||
[]
|
||||
(main/stop))
|
||||
(main/stop)
|
||||
:stopped)
|
||||
|
||||
(defn restart
|
||||
[]
|
||||
(main/restart))
|
||||
(stop)
|
||||
(repl/refresh :after 'user/start))
|
||||
|
||||
(defn restart-all
|
||||
[]
|
||||
(main/restart-all))
|
||||
(stop)
|
||||
(repl/refresh-all :after 'user/start))
|
||||
|
||||
;; (defn compression-bench
|
||||
;; [data]
|
||||
|
||||
@ -4,23 +4,24 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC Sucursal en España SL",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@11.18.0+sha512.33d83c77da82f49fba836925c6f1b841181ec3132b670639bd012f7075f5c7cf634c5f870147c19aae7478fac01df09d8892e880454896edd23ee9b33757563c",
|
||||
"packageManager": "pnpm@10.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/penpot/penpot"
|
||||
},
|
||||
"dependencies": {
|
||||
"luxon": "^3.4.4",
|
||||
"sax": "^1.6.0"
|
||||
"sax": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.14",
|
||||
"nodemon": "^3.1.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ws": "^8.21.0"
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "clj-kondo --parallel --lint ../common/src src/",
|
||||
"check-fmt": "cljfmt check --parallel=true src/ test/",
|
||||
"fmt": "cljfmt fix --parallel=true src/ test/"
|
||||
"fmt": "cljfmt fix --parallel=true src/ test/",
|
||||
"test": "clojure -M:dev:test"
|
||||
}
|
||||
}
|
||||
|
||||
84
backend/pnpm-lock.yaml
generated
84
backend/pnpm-lock.yaml
generated
@ -12,18 +12,18 @@ importers:
|
||||
specifier: ^3.4.4
|
||||
version: 3.7.2
|
||||
sax:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.3
|
||||
devDependencies:
|
||||
nodemon:
|
||||
specifier: ^3.1.14
|
||||
version: 3.1.14
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.11
|
||||
source-map-support:
|
||||
specifier: ^0.5.21
|
||||
version: 0.5.21
|
||||
ws:
|
||||
specifier: ^8.21.0
|
||||
version: 8.21.0
|
||||
specifier: ^8.17.0
|
||||
version: 8.18.3
|
||||
|
||||
packages:
|
||||
|
||||
@ -31,17 +31,15 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
balanced-match@4.0.4:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
brace-expansion@5.0.7:
|
||||
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
brace-expansion@1.1.12:
|
||||
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
|
||||
|
||||
braces@3.0.3:
|
||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||
@ -54,6 +52,9 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
concat-map@0.0.1:
|
||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||
|
||||
debug@4.4.3:
|
||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||
engines: {node: '>=6.0'}
|
||||
@ -103,15 +104,14 @@ packages:
|
||||
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
minimatch@10.2.5:
|
||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
nodemon@3.1.14:
|
||||
resolution: {integrity: sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==}
|
||||
nodemon@3.1.11:
|
||||
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@ -119,8 +119,8 @@ packages:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
picomatch@2.3.2:
|
||||
resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
|
||||
picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
|
||||
pstree.remy@1.1.8:
|
||||
@ -130,12 +130,11 @@ packages:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
sax@1.6.0:
|
||||
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
|
||||
engines: {node: '>=11.0.0'}
|
||||
sax@1.4.3:
|
||||
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
|
||||
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
semver@7.7.3:
|
||||
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
@ -165,8 +164,8 @@ packages:
|
||||
undefsafe@2.0.5:
|
||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||
|
||||
ws@8.21.0:
|
||||
resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
|
||||
ws@8.18.3:
|
||||
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
@ -182,15 +181,16 @@ snapshots:
|
||||
anymatch@3.1.3:
|
||||
dependencies:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.2
|
||||
picomatch: 2.3.1
|
||||
|
||||
balanced-match@4.0.4: {}
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
brace-expansion@5.0.7:
|
||||
brace-expansion@1.1.12:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
balanced-match: 1.0.2
|
||||
concat-map: 0.0.1
|
||||
|
||||
braces@3.0.3:
|
||||
dependencies:
|
||||
@ -210,6 +210,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
||||
debug@4.4.3(supports-color@5.5.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@ -245,20 +247,20 @@ snapshots:
|
||||
|
||||
luxon@3.7.2: {}
|
||||
|
||||
minimatch@10.2.5:
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.7
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
nodemon@3.1.14:
|
||||
nodemon@3.1.11:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
ignore-by-default: 1.0.1
|
||||
minimatch: 10.2.5
|
||||
minimatch: 3.1.2
|
||||
pstree.remy: 1.1.8
|
||||
semver: 7.8.5
|
||||
semver: 7.7.3
|
||||
simple-update-notifier: 2.0.0
|
||||
supports-color: 5.5.0
|
||||
touch: 3.1.1
|
||||
@ -266,21 +268,21 @@ snapshots:
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
picomatch@2.3.2: {}
|
||||
picomatch@2.3.1: {}
|
||||
|
||||
pstree.remy@1.1.8: {}
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.2
|
||||
picomatch: 2.3.1
|
||||
|
||||
sax@1.6.0: {}
|
||||
sax@1.4.3: {}
|
||||
|
||||
semver@7.8.5: {}
|
||||
semver@7.7.3: {}
|
||||
|
||||
simple-update-notifier@2.0.0:
|
||||
dependencies:
|
||||
semver: 7.8.5
|
||||
semver: 7.7.3
|
||||
|
||||
source-map-support@0.5.21:
|
||||
dependencies:
|
||||
@ -301,4 +303,4 @@ snapshots:
|
||||
|
||||
undefsafe@2.0.5: {}
|
||||
|
||||
ws@8.21.0: {}
|
||||
ws@8.18.3: {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user