mirror of
https://github.com/penpot/penpot.git
synced 2026-08-25 14:18:40 +00:00
Compare commits
No commits in common. "develop" and "2.16.0-RC11" have entirely different histories.
develop
...
2.16.0-RC1
@ -88,9 +88,6 @@
|
||||
:dynamic-var-not-earmuffed
|
||||
{:level :off}
|
||||
|
||||
:type-mismatch
|
||||
{:level :off}
|
||||
|
||||
:used-underscored-binding
|
||||
{:level :warning}
|
||||
|
||||
|
||||
@ -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
|
||||
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@ -1,7 +1,9 @@
|
||||
description: Create a report to help us improve
|
||||
name: Bug report
|
||||
title: "bug: "
|
||||
type: Bug
|
||||
labels: ["needs triage"]
|
||||
labels: ["triage"]
|
||||
projects: ["penpot/8"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@ -1,7 +1,9 @@
|
||||
description: Suggest an idea for this project.
|
||||
labels: ["needs triage"]
|
||||
name: "Feature request"
|
||||
title: "feature: "
|
||||
type: Enhancement
|
||||
projects: ["penpot/8"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
41
.github/scripts/playwright-summary.jq
vendored
41
.github/scripts/playwright-summary.jq
vendored
@ -1,41 +0,0 @@
|
||||
def specs: [.. | objects | select(has("tests") and has("file"))];
|
||||
def dur: [.tests[].results[]?.duration // 0] | add;
|
||||
|
||||
specs as $s
|
||||
| ($s | map(select(any(.tests[]; .status == "unexpected")))) as $failed
|
||||
| ($s | map(select(any(.tests[]; .status == "flaky")))) as $flaky
|
||||
| ($s | map(select(any(.tests[]; .status == "skipped")))) as $skipped
|
||||
| ($s | length) as $total
|
||||
| ($s | map(dur) | add // 0 | . / 1000 | floor) as $cpu
|
||||
| (if ($failed | length) > 0 then "❌"
|
||||
elif ($flaky | length) > 0 then "⚠️"
|
||||
else "✅" end) as $icon
|
||||
|
||||
| "## \($icon) Integration tests\n\n"
|
||||
+ "| Total | Passed | Flaky | Failed | Skipped | Test time |\n"
|
||||
+ "|---|---|---|---|---|---|\n"
|
||||
+ "| \($total) | \($total - ($failed|length) - ($flaky|length) - ($skipped|length)) "
|
||||
+ "| \($flaky|length) | \($failed|length) | \($skipped|length) | \($cpu / 60 | floor)m |\n"
|
||||
|
||||
+ (if ($failed | length) > 0 then
|
||||
"\n### Failed\n\n"
|
||||
+ ($failed | map("- `\(.file):\(.line)` — \(.title)") | join("\n")) + "\n"
|
||||
else "" end)
|
||||
|
||||
+ (if ($flaky | length) > 0 then
|
||||
"\n### Flaky (passed on retry)\n\n"
|
||||
+ ($flaky
|
||||
| map({ t: "`\(.file):\(.line)` — \(.title)",
|
||||
r: ([.tests[].results[]? | select(.status == "failed")] | length) })
|
||||
| sort_by(-.r)
|
||||
| map("- \(.t) _(\(.r) \(if .r == 1 then "retry" else "retries" end))_")
|
||||
| join("\n")) + "\n"
|
||||
else "" end)
|
||||
|
||||
+ (if $total > 0 then
|
||||
"\n<details><summary>Slowest specs</summary>\n\n"
|
||||
+ ($s | map({ t: "`\(.file)` — \(.title)", d: (dur / 1000 | floor) })
|
||||
| sort_by(-.d) | .[0:5]
|
||||
| map("- \(.t) — \(.d)s") | join("\n"))
|
||||
+ "\n\n</details>\n"
|
||||
else "" end)
|
||||
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!");
|
||||
|
||||
110
.github/workflows/build-bundle.yml
vendored
110
.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
|
||||
runs-on: penpot-standar-runner
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
gh_ref: ${{ steps.vars.outputs.gh_ref }}
|
||||
bundle_version: ${{ steps.vars.outputs.bundle_version }}
|
||||
exists: ${{ steps.check.outputs.exists }}
|
||||
build-bundle:
|
||||
name: Build and Upload Penpot Bundle
|
||||
runs-on: penpot-runner-01
|
||||
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-standar-runner
|
||||
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-standar-runner
|
||||
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-docker-admin-console:
|
||||
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"
|
||||
24
.github/workflows/build-docker-devenv.yml
vendored
24
.github/workflows/build-docker-devenv.yml
vendored
@ -6,7 +6,8 @@ on:
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and push DevEnv Docker image
|
||||
runs-on: penpot-extended-runner
|
||||
environment: release-admins
|
||||
runs-on: penpot-runner-02
|
||||
|
||||
steps:
|
||||
- name: Set common environment variables
|
||||
@ -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
|
||||
|
||||
331
.github/workflows/build-docker.yml
vendored
331
.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
|
||||
runs-on: penpot-extended-runner
|
||||
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-extended-runner
|
||||
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]
|
||||
build-and-push:
|
||||
name: Build and Push Penpot Docker Images
|
||||
runs-on: penpot-runner-02
|
||||
|
||||
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-extended-runner
|
||||
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-extended-runner
|
||||
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
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
name: _TMP TOKENS
|
||||
name: _MAIN-STAGING
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '46 5-20 * * 1-5'
|
||||
- cron: '26 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-bundle:
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "hiru-tokens-in-libs"
|
||||
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: "hiru-tokens-in-libs"
|
||||
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-docker-admin-console:
|
||||
uses: ./.github/workflows/build-docker-admin-console.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "staging"
|
||||
|
||||
19
.github/workflows/build-tag.yml
vendored
19
.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
|
||||
@ -20,21 +22,14 @@ jobs:
|
||||
with:
|
||||
gh_ref: ${{ github.ref_name }}
|
||||
|
||||
build-docker-admin-console:
|
||||
uses: ./.github/workflows/build-docker-admin-console.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: ${{ github.ref_name }}
|
||||
|
||||
notify:
|
||||
name: Notifications
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- build-docker
|
||||
- build-docker-admin-console
|
||||
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
|
||||
@ -45,9 +40,7 @@ jobs:
|
||||
|
||||
publish-final-tag:
|
||||
if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
|
||||
needs:
|
||||
- build-docker
|
||||
- build-docker-admin-console
|
||||
needs: build-docker
|
||||
uses: ./.github/workflows/release.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
|
||||
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
|
||||
|
||||
4
.github/workflows/plugins-deploy-package.yml
vendored
4
.github/workflows/plugins-deploy-package.yml
vendored
@ -34,7 +34,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: penpot-standar-runner
|
||||
runs-on: penpot-runner-01
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@ -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-extended-runner
|
||||
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-extended-runner
|
||||
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-extended-runner
|
||||
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
|
||||
58
.github/workflows/tests-exporter.yml
vendored
58
.github/workflows/tests-exporter.yml
vendored
@ -1,58 +0,0 @@
|
||||
name: "CI: Exporter"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'exporter/**'
|
||||
- 'common/**'
|
||||
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
paths:
|
||||
- 'exporter/**'
|
||||
- 'common/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test-exporter:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Exporter 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: ./exporter
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install;
|
||||
pnpm run check-fmt:clj
|
||||
pnpm run lint:clj
|
||||
|
||||
- name: Tests
|
||||
working-directory: ./exporter
|
||||
run: |
|
||||
./scripts/test
|
||||
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-extended-runner
|
||||
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
|
||||
245
.github/workflows/tests-integration.yml
vendored
245
.github/workflows/tests-integration.yml
vendored
@ -1,245 +0,0 @@
|
||||
name: "CI: Integration"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch or ref'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
|
||||
shards:
|
||||
description: 'Shard layout (JSON array)'
|
||||
type: choice
|
||||
required: true
|
||||
default: '[1, 2, 3, 4]'
|
||||
options:
|
||||
- '[1, 2, 3, 4]'
|
||||
- '[1, 2, 3, 4, 5, 6]'
|
||||
- '[1, 2]'
|
||||
- '[1]'
|
||||
|
||||
workers:
|
||||
description: 'Playwright workers per shard'
|
||||
type: string
|
||||
required: true
|
||||
default: '2'
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
- 'render-wasm/**'
|
||||
- '.github/workflows/tests-integration.yml'
|
||||
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- ready_for_review
|
||||
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- 'common/**'
|
||||
- 'render-wasm/**'
|
||||
- '.github/workflows/tests-integration.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || inputs.gh_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build-integration:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Build Integration Bundle"
|
||||
runs-on: penpot-extended-runner
|
||||
timeout-minutes: 30
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
|
||||
outputs:
|
||||
bundle_key: ${{ steps.vars.outputs.bundle_key }}
|
||||
|
||||
steps:
|
||||
# An empty `ref` makes checkout fall back to its default (the PR merge
|
||||
# ref on pull_request, the pushed ref on push).
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
# The cache key must come from the SHA actually checked out: on a manual
|
||||
# run `github.sha` points at the dispatching ref, not at `gh_ref`.
|
||||
- name: Extract cache key
|
||||
id: vars
|
||||
run: |
|
||||
echo "bundle_key=integration-bundle-$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build Bundle
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
./scripts/build
|
||||
|
||||
- name: Store Bundle Cache
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: ${{ steps.vars.outputs.bundle_key }}
|
||||
path: frontend/resources/public
|
||||
|
||||
test-integration:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Integration Tests (${{ matrix.shard }})"
|
||||
runs-on: penpot-extended-runner
|
||||
timeout-minutes: ${{ github.base_ref == 'staging' && 60 || 25 }}
|
||||
|
||||
needs: build-integration
|
||||
|
||||
# TEMPORARY (release stabilization): PRs targeting `staging` run on a
|
||||
# single serial shard, so new flakes cannot block the release work.
|
||||
# Remove the `github.base_ref` branch below to restore full parallelism.
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: ${{ fromJSON(inputs.shards || (github.base_ref == 'staging' && '[1]' || '[1, 2, 3, 4]')) }}
|
||||
|
||||
container:
|
||||
image: penpotapp/devenv:latest
|
||||
volumes:
|
||||
- /var/cache/github-runner/m2:/root/.m2
|
||||
- /var/cache/github-runner/gitlib:/root/.gitlibs
|
||||
- /var/cache/github-runner/ms-playwright:/ms-playwright
|
||||
env:
|
||||
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
key: ${{ needs.build-integration.outputs.bundle_key }}
|
||||
path: frontend/resources/public
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install --frozen-lockfile;
|
||||
|
||||
# No-op once the shared volume is warm; keeps the first run working.
|
||||
- name: Install Playwright Chromium
|
||||
working-directory: ./frontend
|
||||
run: pnpm exec playwright install chromium
|
||||
|
||||
# `strategy.job-total` is the matrix size, so the shard denominator
|
||||
# follows the `shards` input without being hardcoded.
|
||||
- name: Run Tests
|
||||
working-directory: ./frontend
|
||||
env:
|
||||
WORKERS: ${{ inputs.workers }}
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
# TEMPORARY (release stabilization): see the note on the matrix above.
|
||||
if [ -z "$WORKERS" ]; then
|
||||
if [ "$BASE_REF" = "staging" ]; then WORKERS=1; else WORKERS=2; fi
|
||||
fi
|
||||
echo "Running shard ${{ matrix.shard }}/${{ strategy.job-total }} with $WORKERS workers"
|
||||
pnpm exec playwright test --project default \
|
||||
--workers="$WORKERS" \
|
||||
--shard=${{ matrix.shard }}/${{ strategy.job-total }} \
|
||||
--reporter=blob
|
||||
|
||||
- name: Upload blob report
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: integration-blob-report-${{ matrix.shard }}
|
||||
path: frontend/blob-report/
|
||||
overwrite: true
|
||||
retention-days: 3
|
||||
|
||||
- name: Upload test result
|
||||
uses: actions/upload-artifact@v7
|
||||
if: always()
|
||||
with:
|
||||
name: integration-tests-result-${{ matrix.shard }}
|
||||
path: frontend/test-results/
|
||||
overwrite: true
|
||||
if-no-files-found: ignore
|
||||
retention-days: 3
|
||||
|
||||
merge-reports:
|
||||
if: ${{ always() && !github.event.pull_request.draft && needs.test-integration.result != 'skipped' }}
|
||||
name: "Merge Integration Reports"
|
||||
runs-on: penpot-extended-runner
|
||||
timeout-minutes: 15
|
||||
|
||||
needs: test-integration
|
||||
|
||||
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
|
||||
with:
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
pnpm install --frozen-lockfile;
|
||||
|
||||
- name: Download blob reports
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: frontend/all-blob-reports
|
||||
pattern: integration-blob-report-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML report
|
||||
working-directory: ./frontend
|
||||
env:
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME: report.json
|
||||
run: |
|
||||
pnpm exec playwright merge-reports \
|
||||
--reporter=html,json,list ./all-blob-reports
|
||||
|
||||
- name: Test summary
|
||||
if: always()
|
||||
working-directory: ./frontend
|
||||
run: |
|
||||
if [ ! -f report.json ]; then
|
||||
echo "No report produced (all shards failed early)." >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
jq -r -f ../.github/scripts/playwright-summary.jq report.json >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: integration-html-report
|
||||
path: frontend/playwright-report/
|
||||
overwrite: true
|
||||
retention-days: 7
|
||||
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-extended-runner
|
||||
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
|
||||
9
.github/workflows/tests-mcp.yml
vendored
9
.github/workflows/tests-mcp.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: "CI: MCP"
|
||||
name: "MCP CI"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@ -28,7 +28,7 @@ jobs:
|
||||
test-mcp:
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
name: "Test MCP"
|
||||
runs-on: penpot-extended-runner
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
@ -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-extended-runner
|
||||
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-extended-runner
|
||||
# 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-extended-runner
|
||||
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-extended-runner
|
||||
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
|
||||
23
.gitignore
vendored
23
.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/
|
||||
@ -58,8 +50,6 @@ opencode.json
|
||||
/docker/images/bundle*
|
||||
/exporter/target
|
||||
/exporter/.shadow-cljs
|
||||
/exporter/resources/wasm/
|
||||
/exporter/src/app/wasm/shared.js
|
||||
/frontend/.storybook/preview-body.html
|
||||
/frontend/.storybook/preview-head.html
|
||||
/frontend/playwright-report/
|
||||
@ -76,6 +66,8 @@ opencode.json
|
||||
/frontend/target/
|
||||
/frontend/test-results/
|
||||
/frontend/.shadow-cljs
|
||||
/other/
|
||||
/scripts/
|
||||
/nexus/
|
||||
/tmp/
|
||||
/vendor/**/target
|
||||
@ -90,20 +82,11 @@ opencode.json
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/render-wasm/target/
|
||||
/media-processor/dist/
|
||||
/**/node_modules
|
||||
/**/.yarn/*
|
||||
/.pnpm-store
|
||||
/.vscode
|
||||
/.idea
|
||||
*.iml
|
||||
/.claude
|
||||
/.playwright-mcp
|
||||
/.devenv/mcp/
|
||||
/opencode.json
|
||||
/.opencode/plans
|
||||
/.opencode/reports
|
||||
/.opencode/prompts
|
||||
/.ci-logs
|
||||
/.codex/
|
||||
/tools/__pycache__
|
||||
|
||||
28
.opencode/agents/commiter.md
Normal file
28
.opencode/agents/commiter.md
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
name: commiter
|
||||
description: Git commit assistant
|
||||
mode: all
|
||||
---
|
||||
|
||||
## Role
|
||||
|
||||
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.
|
||||
|
||||
## Requirements
|
||||
|
||||
* 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,42 +0,0 @@
|
||||
---
|
||||
description: Execute a ready plan end-to-end — create a GitHub issue, branch issue-NNNN, implement the plan, then commit via the create-commit skill
|
||||
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 create-commit skill
|
||||
|
||||
After the implementation is complete, load the **`create-commit`** skill and
|
||||
follow its workflow to commit the changes. Provide a brief summary of what was
|
||||
implemented and why, the issue reference (`issue-NNNN`), and the model name you
|
||||
are running as so the `AI-assisted-by` trailer is set correctly.
|
||||
|
||||
Do not push. Pushing is handled separately by the user.
|
||||
@ -1,40 +0,0 @@
|
||||
---
|
||||
description: Resolve local git conflicts and stage the resolved files with git add — never continues the rebase
|
||||
agent: build
|
||||
---
|
||||
|
||||
# Fix Git Conflicts
|
||||
|
||||
Resolve conflicts in the local repository. The user handles finishing the
|
||||
rebase themselves — you must **never** run `git rebase --continue`,
|
||||
`git rebase --skip`, `git merge --continue`, or anything similar.
|
||||
|
||||
## Phase 1 — Understand the problem (read-only)
|
||||
|
||||
1. Run `git status` to detect the conflict state (rebase, merge, cherry-pick, etc.) and list conflicted files.
|
||||
2. For each conflicted (unmerged) file, understand the situation **without modifying anything**:
|
||||
- Read the file and identify the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`).
|
||||
- Inspect both sides — `git show <ours>:<file>` and `git show <theirs>:<file>` — plus `git log`/`git show` on the commits involved to understand intent.
|
||||
- Identify what each side changed and why, and how they should be combined.
|
||||
|
||||
## Phase 2 — Present the resolution plan
|
||||
|
||||
3. **Present a clear plan to the user before touching any file.** For each conflicted file, state:
|
||||
- What each side changed and why.
|
||||
- Your proposed resolution and the reasoning behind it.
|
||||
- How the two sides are combined (both additive → merge; both modify the same code → keep the semantically correct version, merging intent from both sides when clear from code and context).
|
||||
4. **Ask the user only when genuinely unclear.** Do not ask about anything you can determine yourself from the code, commit messages, or context. Only decisions that are not determinable and change the outcome (e.g. conflicting product decisions, which side to discard) warrant a question. **Collect all such questions together in an "Open Questions" section at the end of the plan**, so the user has full context to answer them properly.
|
||||
5. **Wait for the user to accept the plan** (and answer any open questions) before editing, staging, or otherwise modifying anything.
|
||||
|
||||
## Phase 3 — Execute
|
||||
|
||||
6. Resolve each conflicted file by editing the file to the agreed merged content and removing all conflict markers.
|
||||
|
||||
## Phase 4 — Stage and verify
|
||||
|
||||
7. **Stage every resolved file** with `git add <file>`. Do not stage unrelated untracked files unless clearly part of the resolution.
|
||||
8. Verify no conflict markers remain (search for `<<<<<<<` / `>>>>>>>` in resolved files) and that `git status` shows no unmerged paths.
|
||||
|
||||
## Phase 5 — Report
|
||||
|
||||
9. Briefly report the conflict state, how each conflicted file was resolved (and any answers received to open questions), and stop — do **not** run `git rebase --continue` or any other continuation command.
|
||||
@ -1,25 +0,0 @@
|
||||
Act as a senior software engineer and perform a thorough review.
|
||||
|
||||
## Instructions
|
||||
|
||||
1. **Determine what is being reviewed** from the provided context:
|
||||
- **If it is a plan** (implementation plan, design document, task breakdown) → load the **`plan-review`** skill.
|
||||
- **If it is code** (diff, PR, code change) → load the **`code-review`** skill.
|
||||
|
||||
2. Read `AGENTS.md` and follow its instructions for finding and reading all related testing documentation from memories before reviewing.
|
||||
|
||||
3. **Skip generated files, lockfile-only changes, and unrelated modifications** unless they introduce security risks.
|
||||
|
||||
4. Follow the loaded skill's process and produce its output format.
|
||||
|
||||
## Strong Rules
|
||||
|
||||
1. Do not invent problems. Every finding must be real and actionable.
|
||||
2. Do not modify any code and do not create a commit — this command only reviews.
|
||||
3. Be specific and constructive. "This could be better" is not helpful — explain why and how.
|
||||
4. Prioritize by impact. One structural issue outweighs ten nits.
|
||||
5. Missing tests are an issue, not a suggestion. Report as a severity-tagged finding — never as a recommendation.
|
||||
|
||||
## 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,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
85
.opencode/skills/backport-commit/SKILL.md
Normal file
85
.opencode/skills/backport-commit/SKILL.md
Normal file
@ -0,0 +1,85 @@
|
||||
---
|
||||
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. 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
|
||||
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,47 +0,0 @@
|
||||
---
|
||||
name: create-commit
|
||||
description: Stage, review, and commit files following Penpot commit conventions.
|
||||
---
|
||||
|
||||
# Skill: create-commit
|
||||
|
||||
Produce a git commit that follows Penpot's commit message conventions. This
|
||||
skill owns the commit format, staging review, and safety checks — it does not
|
||||
implement features or push.
|
||||
|
||||
## When to Use
|
||||
|
||||
- After code changes are complete and files need to be committed
|
||||
- When delegated by a workflow step (e.g. implement-plan) to handle the commit
|
||||
|
||||
## Required Reading
|
||||
|
||||
Before drafting any commit, read `mem:workflow/creating-commits` 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.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Stage the files** specified by the calling context. Do not ask for
|
||||
confirmation.
|
||||
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. Draft the message following the format in the memory doc, wrapping the body
|
||||
at 72 characters per line, and run:
|
||||
```bash
|
||||
git commit -m "<subject>" -m "<body>"
|
||||
```
|
||||
(or `git commit -F -` if the body has unusual characters).
|
||||
4. The `AI-assisted-by` trailer value is provided by the calling context — 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`.
|
||||
- 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 explicitly asked.
|
||||
- Do not bypass pre-commit hooks (`--no-verify`) unless explicitly asked.
|
||||
- Do not add untracked files that were not created in this session.
|
||||
@ -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
|
||||
```
|
||||
227
.opencode/skills/gh-issue-from-pr/SKILL.md
Normal file
227
.opencode/skills/gh-issue-from-pr/SKILL.md
Normal file
@ -0,0 +1,227 @@
|
||||
---
|
||||
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. Use the `issue-title` skill to generate this. |
|
||||
| **Labels** | PR labels | Copy `community contribution` if present. Skip `bug` and `enhancement` (redundant with Issue Type). 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 "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>`
|
||||
|
||||
### 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 |
|
||||
|--------|-----------|
|
||||
| `community contribution` | `community contribution` |
|
||||
| `bug`, `enhancement` | *(skip — redundant with Issue Type)* |
|
||||
| `backport candidate` | *(skip — workflow label)* |
|
||||
| `team-qa` | *(skip — workflow label)* |
|
||||
|
||||
## 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.
|
||||
123
.opencode/skills/issue-title/SKILL.md
Normal file
123
.opencode/skills/issue-title/SKILL.md
Normal file
@ -0,0 +1,123 @@
|
||||
---
|
||||
name: issue-title
|
||||
description: Derive a clear, well-formatted title for a GitHub issue from its description body, using descriptive present-tense for bugs and imperative mood for features, always including the "where" (location in the UI/module).
|
||||
---
|
||||
|
||||
# Skill: issue-title
|
||||
|
||||
Derive a concise, descriptive title for a GitHub issue based on its body
|
||||
content. Use **descriptive present tense for bugs** (e.g. "Plugin API
|
||||
crashes when setting text fills") and **imperative mood for features** (e.g.
|
||||
"Add customizable dash and gap controls"). No emoji or type prefixes
|
||||
(`feat:`, `bug:`, `feature:`, etc.).
|
||||
|
||||
Can be used both when **creating a new issue** and when **updating an
|
||||
existing one** that has a vague or outdated title.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Creating a new issue and need a well-formatted title from the draft body
|
||||
- An existing issue has a vague, outdated, or auto-generated title (e.g.
|
||||
`[PENPOT FEEDBACK]: ...`, `feature: ...`)
|
||||
- The current title doesn't reflect the actual content of the description
|
||||
- The title is missing the "where" (which part of the UI/module is affected)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `gh` CLI authenticated (`gh auth status`)
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Get the issue body
|
||||
|
||||
For an **existing issue**, fetch it:
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo penpot/penpot --json title,body
|
||||
```
|
||||
|
||||
For a **new issue**, read the draft body from wherever it was provided
|
||||
(Taiga link, user report, discussion, etc.).
|
||||
|
||||
### 2. Read the body and derive a title
|
||||
|
||||
Extract the core problem or request from the description. Distinguish between
|
||||
bug reports and feature requests:
|
||||
|
||||
**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"*
|
||||
- *"Text layer content is not deleted when WebGL render is enabled"*
|
||||
|
||||
Do **not** start bug titles with "Fix" or any imperative verb. The title
|
||||
should 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"*
|
||||
- *"Validate shape on add-object to catch malformed inputs early"*
|
||||
|
||||
**Universal rules (both types):**
|
||||
- **Include the "where"** — specify the UI location or module (e.g.
|
||||
"in the sidebar", "in the workspace history panel", "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. If the
|
||||
description mentions two related problems, capture both.
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Original / draft title | Type | New title |
|
||||
|---|---|---|
|
||||
| `[PENPOT FEEDBACK]: WebGL` | Bug | `Canvas renders glitches when zooming quickly — text appears distorted and nodes have background-colored rectangles` |
|
||||
| `bug: flatten-nested-tokens-json uses $type instead of $value as the DTCG token/group discriminator` | Bug | `Token import fails when group-level type inheritance is used — parser misidentifies groups as tokens` |
|
||||
| `feature: Dashed stroke customization` | Feature | `Add customizable dash and gap length controls to dashed strokes in the sidebar` |
|
||||
| `feature: Add more detail to history of actions` | Feature | `Show user, timestamp, and hash in the workspace history panel like git commits` |
|
||||
|
||||
### 3. Apply the title
|
||||
|
||||
**If updating an existing issue:**
|
||||
|
||||
```bash
|
||||
gh issue edit <NUMBER> --repo penpot/penpot --title "<NEW TITLE>"
|
||||
```
|
||||
|
||||
**If creating a new issue:**
|
||||
|
||||
```bash
|
||||
gh issue create --repo penpot/penpot --title "<NEW TITLE>" --body "<BODY>"
|
||||
```
|
||||
|
||||
### 4. Confirm
|
||||
|
||||
For updates, the command returns the issue URL. Verify by optionally fetching
|
||||
again:
|
||||
|
||||
```bash
|
||||
gh issue view <NUMBER> --repo penpot/penpot --json title
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Bug titles describe the symptom** — present tense, 3rd person:
|
||||
"crashes", "fails", "shows", "is cut off", "does not load". Do not
|
||||
start with "Fix" or "Bug:".
|
||||
- **Feature titles use imperative mood** — command form: "Add", "Show",
|
||||
"Use", "Validate", "Support", "Toggle".
|
||||
- **Always include the "where"** — a title like "Crashes when zooming"
|
||||
is too vague; "Canvas crashes when zooming quickly" is clear.
|
||||
- **No prefixes, no emoji** — strip all type labels and decorative
|
||||
characters from the title.
|
||||
- **Derive from the body, not the current title** — the body contains
|
||||
the real detail; the current title may be auto-generated or stale.
|
||||
- **Two problems → cover both** — if the description has two distinct
|
||||
but related issues, capture both in the title joined by "and".
|
||||
@ -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,315 +0,0 @@
|
||||
---
|
||||
name: plan-review
|
||||
description: Reviews implementation plans for quality, completeness, and actionability. Use after a plan is produced by the planner skill, before starting implementation. Use when evaluating a plan written by yourself, another agent, or a human.
|
||||
---
|
||||
|
||||
# Plan Review
|
||||
|
||||
## Overview
|
||||
|
||||
Multi-dimensional plan review with quality gates. Every plan gets reviewed before implementation starts — no exceptions. Review covers six axes: completeness, task quality, architecture & sequencing, risk coverage, actionability, and proposed code quality.
|
||||
|
||||
**The approval standard:** Approve a plan when it is specific enough that a skilled implementer could execute it without guessing, the task ordering is sound, and risks are acknowledged. Perfect plans don't exist — the goal is confidence that implementation won't derail. Don't block a plan because it isn't exactly how you would have structured it. If it's executable and well-organized, approve it.
|
||||
|
||||
## When to Use
|
||||
|
||||
- After the planner skill produces a plan
|
||||
- Before starting implementation on any non-trivial task
|
||||
- When reviewing a plan written by another agent or a human
|
||||
- When a plan feels too large, vague, or risky to start
|
||||
|
||||
**Do NOT use for:** Single-file changes with obvious scope, or when the task is trivial enough to just do.
|
||||
|
||||
## The Six-Axis Review
|
||||
|
||||
Every plan gets evaluated across these dimensions:
|
||||
|
||||
### 1. Completeness
|
||||
|
||||
Does the plan cover everything needed to implement successfully?
|
||||
|
||||
- Is the **context** clear? (What problem, why now, what's the goal?)
|
||||
- Are **affected modules** identified with paths?
|
||||
- Are **architecture decisions** documented with rationale?
|
||||
- Is there a **testing strategy**?
|
||||
- Are **verification commands** explicit (not "run the tests")?
|
||||
- Are **open questions** listed (not buried in someone's head)?
|
||||
- Is there a **parallelization** assessment for multi-task plans?
|
||||
|
||||
**Missing any of these is a gap, not a nit.**
|
||||
|
||||
### 2. Task Quality
|
||||
|
||||
Are the tasks well-defined and independently executable?
|
||||
|
||||
- Does every task have **acceptance criteria**? (Testable, not vague)
|
||||
- Does every task have **verification steps**?
|
||||
- Are tasks **sized appropriately**? (XS–M is ideal, L is acceptable, XL must be split)
|
||||
- Are **dependencies** between tasks explicitly stated?
|
||||
- Are **files likely touched** listed?
|
||||
- Is each task a **single, self-contained change**? (Not "implement the whole feature")
|
||||
- Could a skilled implementer pick up any task and execute it without asking clarifying questions?
|
||||
|
||||
### 3. Architecture & Sequencing
|
||||
|
||||
Is the plan structured so implementation flows correctly?
|
||||
|
||||
- Does implementation order follow the **dependency graph** (foundations first)?
|
||||
- Are tasks **vertically sliced** (feature paths) rather than horizontally layered?
|
||||
- Does each task leave the system in a **working state**?
|
||||
- Are there **checkpoints** between major phases?
|
||||
- Are **high-risk tasks early** (fail fast)?
|
||||
- Is the total plan a reasonable number of tasks? (More than ~15 tasks suggests the scope should be split into multiple plans)
|
||||
|
||||
### 4. Risk Coverage
|
||||
|
||||
Are the hard parts acknowledged and mitigated?
|
||||
|
||||
- Are **edge cases** identified?
|
||||
- Are **breaking changes** or **migration concerns** noted?
|
||||
- Are **security implications** considered?
|
||||
- Are **performance implications** considered?
|
||||
- Are **external dependencies** or integration risks flagged?
|
||||
- Is there a plan for **rollback** if something goes wrong?
|
||||
- Are **data integrity** risks addressed (what happens if a migration fails mid-way)?
|
||||
|
||||
### 5. Actionability
|
||||
|
||||
Can an implementer actually execute this?
|
||||
|
||||
- Are **file paths** specific (not "update the relevant files")?
|
||||
- Are **function/method names** mentioned where applicable?
|
||||
- Are **verification commands** copy-pasteable (not "run the linter")?
|
||||
- Are **test commands** project-specific (not generic)?
|
||||
- Is the **code shape** described where the implementation isn't obvious?
|
||||
- Are **conventions** referenced (naming, patterns, existing utilities to reuse)?
|
||||
- Does the plan reference **existing code** the implementer should read first?
|
||||
|
||||
### 6. Proposed Code Quality *(when the plan includes implementation details)*
|
||||
|
||||
If the plan proposes code shapes, function signatures, data structures, or API designs, evaluate those proposals against `code-review` criteria:
|
||||
|
||||
- **Correctness:** Do the proposed types/signatures handle edge cases (null, empty, boundaries)?
|
||||
- **Readability:** Are proposed names descriptive and consistent with project conventions?
|
||||
- **Architecture:** Do proposed abstractions follow existing patterns? Are they justified (not over-engineered)?
|
||||
- **Security:** Do proposed APIs validate input at boundaries? Any injection/XSS vectors in the design?
|
||||
- **Performance:** Do proposed data structures avoid N+1 patterns? Any unbounded operations in the design?
|
||||
|
||||
**When to apply:** Only when the plan includes specific code snippets, type definitions, API contracts, or function signatures. Plans that only describe "what" without showing "how" skip this axis.
|
||||
|
||||
## Structural Remedies
|
||||
|
||||
When you flag a structural problem in a plan, propose the fix — not just the problem:
|
||||
|
||||
- **A task is too large (XL):** Split it into vertical slices. Each slice should be independently testable.
|
||||
- **Missing acceptance criteria:** Draft 2–3 specific, testable conditions for the task.
|
||||
- **Wrong sequencing:** Identify the dependency and propose the correct order.
|
||||
- **No checkpoints:** Suggest where checkpoints should go (typically after every 2–3 tasks).
|
||||
- **Vague verification:** Replace "run tests" with the actual project command.
|
||||
- **Horizontal slicing:** Restructure into vertical feature paths.
|
||||
- **Missing risk section:** Draft the risks you can identify from the plan content.
|
||||
|
||||
Prefer the remedy that makes the plan immediately actionable over one that just flags the gap.
|
||||
|
||||
## Plan Sizing
|
||||
|
||||
Plans should be scoped to a single deliverable:
|
||||
|
||||
```
|
||||
1–5 tasks → Good. A focused feature or bug fix.
|
||||
6–10 tasks → Acceptable for a moderate feature.
|
||||
11–15 tasks → Large. Consider splitting into phases.
|
||||
15+ tasks → Too large. Split into multiple plans.
|
||||
```
|
||||
|
||||
**What counts as "one plan":** A self-contained set of changes that delivers a single coherent capability. If you can describe the goal in one sentence, it's one plan.
|
||||
|
||||
## Categorize Findings
|
||||
|
||||
Label every comment with its severity so the author knows what's required vs optional:
|
||||
|
||||
| Prefix | Meaning | Author Action |
|
||||
|--------|---------|---------------|
|
||||
| *(no prefix)* | Required change | Must address before implementation starts |
|
||||
| **Critical:** | Blocks implementation | Missing security consideration, data integrity risk, fundamentally wrong approach |
|
||||
| **Nit:** | Minor, optional | Author may ignore — wording, formatting |
|
||||
| **Optional:** / **Consider:** | Suggestion | Worth considering but not required |
|
||||
| **FYI** | Informational only | No action needed — context for future reference |
|
||||
|
||||
**Lead with what matters.** Order findings by leverage: missing risks and wrong sequencing first, then task quality gaps, then completeness, then nits. If you have one critical sequencing problem and ten nits, the sequencing problem *is* the review.
|
||||
|
||||
## Review Process
|
||||
|
||||
### Step 1: Understand the Goal
|
||||
|
||||
Before evaluating structure, understand intent:
|
||||
|
||||
```
|
||||
- What is this plan trying to accomplish?
|
||||
- What problem does it solve?
|
||||
- What does "done" look like?
|
||||
```
|
||||
|
||||
### Step 2: Check Completeness First
|
||||
|
||||
Scan for missing sections before diving into content:
|
||||
|
||||
```
|
||||
- Context present?
|
||||
- Affected modules listed?
|
||||
- Architecture decisions documented?
|
||||
- Risks acknowledged?
|
||||
- Testing strategy defined?
|
||||
- Verification commands explicit?
|
||||
```
|
||||
|
||||
### Step 3: Review Task Quality
|
||||
|
||||
Walk through each task:
|
||||
|
||||
```
|
||||
For each task:
|
||||
1. Can I tell exactly what to build?
|
||||
2. Are acceptance criteria specific and testable?
|
||||
3. Is the size reasonable (not XL)?
|
||||
4. Are dependencies clear?
|
||||
5. Would I know which files to touch?
|
||||
```
|
||||
|
||||
### Step 4: Validate Sequencing
|
||||
|
||||
Check the dependency graph:
|
||||
|
||||
```
|
||||
- Are foundations built first?
|
||||
- Does each task leave the system working?
|
||||
- Are checkpoints placed correctly?
|
||||
- Are high-risk items early?
|
||||
- Is it vertically sliced?
|
||||
```
|
||||
|
||||
### Step 5: Assess Actionability
|
||||
|
||||
Put yourself in the implementer's shoes:
|
||||
|
||||
```
|
||||
- Could I pick up task 1 and start coding without asking any questions?
|
||||
- Are the verification commands copy-pasteable?
|
||||
- Are file paths and function names specific?
|
||||
- Is existing code referenced where I'd need to read it?
|
||||
```
|
||||
|
||||
### Step 6: Verify the Verification Story
|
||||
|
||||
Check that the plan can actually confirm it worked:
|
||||
|
||||
```
|
||||
- What tests should pass after implementation?
|
||||
- What build/compile commands are relevant?
|
||||
- What manual checks are needed?
|
||||
- How do we know the feature works end-to-end?
|
||||
```
|
||||
|
||||
### Step 7: Evaluate Proposed Code Quality *(if applicable)*
|
||||
|
||||
If the plan includes code snippets, types, or API designs:
|
||||
|
||||
```
|
||||
- Load code-review skill for criteria
|
||||
- Check proposed signatures for edge cases
|
||||
- Verify naming follows project conventions
|
||||
- Confirm abstractions follow existing patterns
|
||||
- Scan for security vectors in proposed APIs
|
||||
- Check for performance issues in proposed data structures
|
||||
```
|
||||
|
||||
## Review Checklist
|
||||
|
||||
```markdown
|
||||
## Review: [Plan title]
|
||||
|
||||
### Completeness
|
||||
- [ ] Context explains the problem and goal
|
||||
- [ ] Affected modules are listed with paths
|
||||
- [ ] Architecture decisions have rationale
|
||||
- [ ] Testing strategy is defined
|
||||
- [ ] Verification commands are explicit and project-specific
|
||||
- [ ] Open questions are listed
|
||||
|
||||
### Task Quality
|
||||
- [ ] Every task has acceptance criteria
|
||||
- [ ] Every task has verification steps
|
||||
- [ ] Tasks are sized XS–M (L acceptable, XL must be split)
|
||||
- [ ] Task dependencies are stated
|
||||
- [ ] Files likely touched are listed
|
||||
|
||||
### Architecture & Sequencing
|
||||
- [ ] Order follows dependency graph (foundations first)
|
||||
- [ ] Vertically sliced (not horizontal layers)
|
||||
- [ ] Each task leaves system working
|
||||
- [ ] Checkpoints exist between phases
|
||||
- [ ] High-risk tasks are early
|
||||
|
||||
### Risk Coverage
|
||||
- [ ] Edge cases identified
|
||||
- [ ] Breaking changes / migrations noted
|
||||
- [ ] Security implications considered
|
||||
- [ ] Performance implications considered
|
||||
- [ ] Rollback strategy exists (if applicable)
|
||||
|
||||
### Actionability
|
||||
- [ ] File paths are specific
|
||||
- [ ] Verification commands are copy-pasteable
|
||||
- [ ] Existing code to read is referenced
|
||||
- [ ] Conventions and patterns are noted
|
||||
|
||||
### Proposed Code Quality *(if plan includes implementation details)*
|
||||
- [ ] Proposed types/signatures handle edge cases
|
||||
- [ ] Proposed names follow project conventions
|
||||
- [ ] Proposed abstractions follow existing patterns
|
||||
- [ ] No security vectors in proposed APIs
|
||||
- [ ] No performance issues in proposed structures
|
||||
|
||||
### Verdict
|
||||
- [ ] **Approve** — Ready to implement
|
||||
- [ ] **Request changes** — Gaps must be addressed
|
||||
```
|
||||
|
||||
## Common Rationalizations
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "I'll figure out the details during implementation" | That's how you discover blocking dependencies mid-task. Surface them now. |
|
||||
| "The tasks are obvious, no need for criteria" | Write them anyway. Explicit criteria surface hidden assumptions. |
|
||||
| "It's just a small feature, it doesn't need a plan" | Small features have edge cases too. 3 tasks with criteria takes 5 minutes. |
|
||||
| "The plan is good enough" | "Good enough" without acceptance criteria means the implementer defines "done" — and they might define it differently. |
|
||||
| "I'll add verification steps later" | Later never comes. The plan is the contract — define verification now. |
|
||||
| "Risks are minimal" | Every change has risks. If you can't name them, you haven't thought about them. |
|
||||
| "The file paths are obvious" | They're obvious to the author. The implementer might not know the codebase. |
|
||||
| "The code in the plan is fine, it'll get reviewed later" | Plan-level code review catches design problems before implementation — fixing them after coding is more expensive. |
|
||||
|
||||
## Red Flags
|
||||
|
||||
- No acceptance criteria on any task
|
||||
- Tasks that say "implement the feature" without specifics
|
||||
- No verification steps anywhere in the plan
|
||||
- All tasks are XL-sized
|
||||
- No checkpoints between phases
|
||||
- Dependency order isn't considered (e.g., API handler before domain model)
|
||||
- No testing strategy
|
||||
- Verification commands are generic ("run tests") instead of project-specific
|
||||
- Plan has 20+ tasks (scope too large for one plan)
|
||||
- No risk section on a plan with migrations, breaking changes, or security implications
|
||||
- Horizontal slicing (all domain, then all services, then all API)
|
||||
- File paths are vague ("update the relevant files")
|
||||
- Missing open questions section despite stated unknowns
|
||||
- Proposed code ignores project conventions or existing patterns
|
||||
- Proposed types use gratuitous `any`/`unknown`/optional without justification
|
||||
- Proposed APIs don't validate input at boundaries
|
||||
|
||||
## See Also
|
||||
|
||||
- For producing plans, use the `planner` skill
|
||||
- For reviewing implemented code, use `code-review` — also the criteria source for axis 6
|
||||
- For security-specific concerns, see `security-and-hardening`
|
||||
- For testing strategy guidance, see `testing`
|
||||
@ -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)
|
||||
@ -1,78 +0,0 @@
|
||||
---
|
||||
name: ste
|
||||
description: Write or rewrite text in ASD-STE100 Simplified Technical English. ONLY use this skill when the user explicitly invokes it by name — i.e. they type "/ste" or literally write "use the ste skill" / "apply ASD-STE100". Do NOT trigger it on paraphrased intent such as "simplify this", "make it clearer", "write technical documentation", or "shorter sentences please" — the user has deliberately scoped this skill to explicit invocation only. For those requests, respond normally without loading this skill unless they name it.
|
||||
---
|
||||
|
||||
# ASD-STE100 Simplified Technical English
|
||||
|
||||
Apply the ASD-STE100 standard to all prose you produce in this task. Do not announce that you use STE, do not name the standard, and do not explain the style unless the user asks. If the user later asks you to "write more naturally," ask one short question to confirm they want to leave STE before you drop it.
|
||||
|
||||
Compliance note (for you, not for output): the official specification and its dictionary are copyright ASD. This skill encodes paraphrased rules and a publicly sourced word list. For certified aerospace/defense deliverables, tell the user that full compliance requires the free official specification (asd-ste100.org) and a human sign-off. Never claim certified compliance.
|
||||
|
||||
## Step 0 — Classify the text
|
||||
|
||||
Before writing a single sentence, decide: is this **procedural** text (instructions someone follows) or **descriptive** text (explanation, background, description)? Every limit below depends on this. Mixed documents get classified section by section.
|
||||
|
||||
## Core rules
|
||||
|
||||
### Sentences
|
||||
- Procedural: maximum **20 words** per sentence.
|
||||
- Descriptive: maximum **25 words** per sentence.
|
||||
- Maximum **6 sentences** per paragraph. One topic per paragraph.
|
||||
- One instruction per sentence. Two actions in one sentence only if they occur at the same time.
|
||||
- Put a condition BEFORE its command: "If the pressure decreases, close the valve."
|
||||
- Do not omit articles, subjects, or verbs to save words. "Ensure file exists" is wrong; "Make sure that the file exists" is correct. Keep the word "that" after verbs like "make sure."
|
||||
- Numbers, units with numbers, abbreviations, quoted strings, code identifiers, and proper nouns each count as one word.
|
||||
|
||||
### Verbs
|
||||
- Allowed forms only: infinitive, imperative, simple present, simple past, simple future, and past participle used as an adjective.
|
||||
- Never use present perfect or continuous forms. "We have received" → "We received." "is being tested" → a simple form.
|
||||
- Never use an -ing form as a verb. An -ing word is allowed only inside a technical name ("the mounting bracket," "logging").
|
||||
- Active voice. Passive is allowed only in descriptive text when the agent is unknown or unimportant.
|
||||
- Instructions use the imperative: "Open the panel," not "You must open the panel" or "The panel should be opened."
|
||||
- Express actions as verbs, not nouns: "compress the file," not "perform compression of the file."
|
||||
- Modals: use **can** (possibility), **will** (future), **must** (requirement). Do not use should, would, could, may, might. A hedge becomes a fact or a "can": "an explosion can occur."
|
||||
- No phrasal verbs: "go down" → "decrease," "set up" → "install," "carry out" → "do."
|
||||
|
||||
### Words
|
||||
- One word, one meaning, one part of speech, used consistently. Never rotate synonyms: pick one name for a thing and repeat it.
|
||||
- Before drafting, replace unapproved vocabulary. Read `references/word-substitutions.md` and apply it; it is the working dictionary for this skill.
|
||||
- Domain-specific nouns (part names, tool names, product names, UI labels) and domain verbs (drill, ream, boot, compile) are your **technical nouns/verbs** — keep them as-is, use each consistently, and do not verb a noun or noun a verb.
|
||||
- Noun clusters: maximum **3 words** ("overhead panel light" is the limit). Longer clusters get decomposed with prepositions or hyphenated on first use: "main-gear-door retraction-winch handle."
|
||||
- American English spelling.
|
||||
- No Latin abbreviations: "e.g." → "for example," "i.e." → "that is," delete "etc."
|
||||
|
||||
### Punctuation
|
||||
- No semicolons — write two sentences.
|
||||
- Parentheses only for references, abbreviations, and item numbers.
|
||||
- Hyphenate words that act as one unit; a hyphenated word counts as one word.
|
||||
- No contractions.
|
||||
|
||||
### Warnings, cautions, notes
|
||||
- **WARNING** = risk of injury or death. **CAUTION** = risk of damage. **NOTE** = information only, never an instruction.
|
||||
- Start a warning or caution with the command or condition, then give the risk:
|
||||
"WARNING: Do not touch the terminal. The terminal has a dangerous voltage."
|
||||
- Notes obey the 25-word descriptive limit.
|
||||
|
||||
## Step 2 — Self-check pass
|
||||
|
||||
After drafting, scan your text once for each of these and fix every hit before you respond:
|
||||
|
||||
1. Any sentence over the 20/25-word limit for its type
|
||||
2. Contractions, semicolons
|
||||
3. "should," "would," "could," "may," "might"
|
||||
4. "has been," "have been," "had been," "is being," "was being"
|
||||
5. -ing words used as verbs
|
||||
6. Missing articles (a/an/the/this) before nouns
|
||||
7. Synonym rotation (the same object under two names)
|
||||
8. Any word in the unapproved column of `references/word-substitutions.md`
|
||||
9. Warnings that state the risk before the command
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/word-substitutions.md` — unapproved → approved word mappings and one-meaning rulings. Read it before drafting; it is short.
|
||||
- `references/examples.md` — worked before/after rewrites (procedural, descriptive, warnings, common mistakes). Read it when rewriting existing text or when unsure how a rule applies.
|
||||
|
||||
## What NOT to touch
|
||||
|
||||
Code blocks, command strings, file paths, error messages, quoted UI text, and proper nouns stay exactly as written. STE applies to the prose around them.
|
||||
@ -1,67 +0,0 @@
|
||||
# Worked before/after examples
|
||||
|
||||
## Verb forms
|
||||
|
||||
| Before | After |
|
||||
|---|---|
|
||||
| We have received the technical reports from HQ. | We received the technical reports from HQ. |
|
||||
| This device has been being used at Boeing since 2005. | Boeing started to use this device in 2005. |
|
||||
| The test is continued by the operator. | Continue the test. |
|
||||
| The screws should be replaced. | Replace the screws. |
|
||||
| The system is currently running diagnostics. | The system does diagnostic tests now. |
|
||||
|
||||
## Vocabulary and phrasing
|
||||
|
||||
| Before | After |
|
||||
|---|---|
|
||||
| Ensure file exists before running. | Make sure that the file exists before you run the command. |
|
||||
| Rotate the cover until the jacks are accessible. | Turn the cover until you can get access to the jacks. |
|
||||
| Extend the jack until the wheels are clear of the ground. | Extend the jack until the wheels do not touch the ground. |
|
||||
| Make sure that these steps are followed. | Obey these steps. |
|
||||
| Utilize approximately 3 liters of water. | Use about 3 liters of water. |
|
||||
| Perform a compression of the log files prior to upload. | Compress the log files before you upload them. |
|
||||
|
||||
## Noun clusters
|
||||
|
||||
| Before | After |
|
||||
|---|---|
|
||||
| Main gear door retraction winch handle | Main-gear-door retraction-winch handle |
|
||||
| Runway light connection resistance calibration | Calibration of the resistance of the runway light connection |
|
||||
| Remove the engine transmission housing attachment bolts. | Remove the bolts that attach the transmission housing to the engine. |
|
||||
|
||||
## Procedural rewrite (condition first, one instruction per sentence)
|
||||
|
||||
Before:
|
||||
> You should check that the reservoir has been filled with the correct fluid and, after attaching a clear tube to the bleed screw while ensuring its free end is submerged, the pedal can be pumped and held so that opening the screw releases trapped air.
|
||||
|
||||
After:
|
||||
> 1. Fill the reservoir with the correct fluid.
|
||||
> 2. Attach a clear tube to the bleed screw.
|
||||
> 3. Put the free end of the tube in a container of fluid.
|
||||
> 4. Push the pedal three times. Hold the pedal down.
|
||||
> 5. Open the bleed screw one half turn. Air and fluid flow into the tube.
|
||||
> 6. Close the bleed screw. Release the pedal.
|
||||
> 7. If air continues to come out, do steps 4 thru 6 again.
|
||||
|
||||
## Warnings and cautions (command first, then risk)
|
||||
|
||||
Before:
|
||||
> Note that serious data loss may potentially occur if the --force flag is used against production.
|
||||
|
||||
After:
|
||||
> CAUTION: Do not use the --force flag on the production database. The flag deletes the rows that do not match the source.
|
||||
|
||||
Before:
|
||||
> Touching the terminal could result in electrocution.
|
||||
|
||||
After:
|
||||
> WARNING: Do not touch the terminal. The terminal has a dangerous voltage.
|
||||
|
||||
## Common mistakes checklist
|
||||
|
||||
- Dropped articles: "Insert pin in bracket" → "Insert the pin in the bracket."
|
||||
- Synonym rotation: check/verify/confirm for the same action → one term, everywhere.
|
||||
- Hedges: "you may want to," "it is recommended that" → an imperative or "must."
|
||||
- Instruction buried in a NOTE: notes never instruct. Move the instruction to a numbered step.
|
||||
- Semicolon joining two clauses → two sentences.
|
||||
- "There are three bolts on the panel" → "The panel has three bolts."
|
||||
@ -1,68 +0,0 @@
|
||||
# Word substitutions and one-meaning rulings
|
||||
|
||||
Compiled from public secondary sources (STEMG/ASD public pages, TechScribe, Acrolinx, training materials). This is a working approximation, not the official ASD dictionary. When a word is not listed here and feels formal or Latin-derived, prefer the shortest common alternative.
|
||||
|
||||
## Unapproved → approved
|
||||
|
||||
| Do not use | Use instead |
|
||||
|---|---|
|
||||
| utilize, leverage, employ | use |
|
||||
| commence, initiate, begin, originate | start |
|
||||
| terminate, cease, conclude | stop, end |
|
||||
| ensure, verify, confirm, validate, check | make sure (that), examine |
|
||||
| perform, conduct, execute, carry out | do |
|
||||
| facilitate, assist | help |
|
||||
| obtain, acquire, procure | get |
|
||||
| sufficient, adequate | enough |
|
||||
| approximately | about |
|
||||
| prior to | before |
|
||||
| subsequent to, following (prep.) | after |
|
||||
| adjacent to | near |
|
||||
| accomplish | do |
|
||||
| additional, supplementary | more |
|
||||
| attempt | try |
|
||||
| require, necessitate | need, must |
|
||||
| mandatory | necessary |
|
||||
| indicate, signify | show |
|
||||
| observe (=watch) | look at, examine |
|
||||
| rotate | turn |
|
||||
| deactivate | turn off, set to off |
|
||||
| activate, energize (unless technical verb) | turn on, start |
|
||||
| toxic | poisonous |
|
||||
| in order to | to |
|
||||
| via, by means of | through, with |
|
||||
| due to, owing to | because of |
|
||||
| in the event of/that | if |
|
||||
| accessible | (rewrite: "you can get access to") |
|
||||
| remainder | rest |
|
||||
| demonstrate | show |
|
||||
| modify, alter | change |
|
||||
| construct, fabricate, build | assemble, make |
|
||||
| retain | keep |
|
||||
| locate (=find) | find |
|
||||
| depress (a button) | push, press |
|
||||
| proceed | continue, go |
|
||||
|
||||
## One meaning, one part of speech (canonical rulings)
|
||||
|
||||
- **close** — verb only: to move to a position that stops flow, or to operate a circuit breaker. The adjective is unapproved → use **near** ("do not go near the propeller").
|
||||
- **test** — noun only: "do a test," never "test the system."
|
||||
- **check** — do not use as a verb for verification → "make sure that" or "examine."
|
||||
- **follow** — means only "come after." For rules and steps use **obey**: "Obey the safety instructions."
|
||||
- **fall** — means only "move down by gravity." For quantities use **decrease**. Never the season.
|
||||
- **oil** — noun only. "Oil the bearing" → "Put oil on the bearing" / "Lubricate the bearing."
|
||||
- **right** — direction only, never "correct."
|
||||
- **clear** — "without blockage." "Wheels are clear of the ground" → "wheels do not touch the ground."
|
||||
- **help** — verb only; the noun is **aid** ("with the aid of a mirror").
|
||||
- **above / below** — physical position only. For quantities: **more than / less than**.
|
||||
- **about** — two approved senses: "approximately" and "on the subject of." Use carefully.
|
||||
- **turn** — the general verb for rotation; "turn on / turn off" for power state is standard.
|
||||
- **level** — approved as noun and adjective (documented exception to the one-POS rule).
|
||||
|
||||
## Frequent-offender function words
|
||||
|
||||
- **should / would / could / may / might** — never. Requirement → **must**. Possibility → **can**. Future → **will**.
|
||||
- **etc.** — delete, or write the full list.
|
||||
- **e.g. / i.e.** — "for example" / "that is."
|
||||
- **any / appropriate / applicable / relevant** as hedges — replace with the specific thing meant.
|
||||
- **there is / there are** openers — rewrite with a real subject: "There are three bolts on the panel" → "The panel has three bolts."
|
||||
@ -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
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
---
|
||||
name: testing
|
||||
description: Enforce TDD workflow and testing best practices for Penpot. Use when implementing features, fixing bugs, or modifying behavior. Reads testing memory for full guidance.
|
||||
---
|
||||
|
||||
# Testing Skill
|
||||
|
||||
Enforces test-driven development and Penpot testing conventions.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Implementing new logic or behavior
|
||||
- Fixing any bug (reproduction test required)
|
||||
- Modifying existing functionality
|
||||
- Adding edge case handling
|
||||
|
||||
**Skip:** Pure configuration changes, documentation updates, or static content with no behavioral impact.
|
||||
|
||||
## Workflow
|
||||
|
||||
Follow TDD (Red → Green → Refactor) whenever practical:
|
||||
|
||||
1. **RED** — Write a failing test first
|
||||
2. **GREEN** — Write minimal code to pass
|
||||
3. **REFACTOR** — Clean up while tests stay green
|
||||
|
||||
For bug fixes, use the Prove-It Pattern: write a test that reproduces the bug, confirm it fails, implement the fix, confirm it passes.
|
||||
|
||||
## Required Reading
|
||||
|
||||
Before writing any test, read:
|
||||
|
||||
1. `.serena/memories/testing.md` — cross-cutting testing principles, TDD workflow, anti-patterns, execution discipline
|
||||
2. Module-specific testing memory for the affected module:
|
||||
- `mem:common/testing` — CLJC unit tests
|
||||
- `mem:frontend/testing` — CLJS unit tests, Playwright E2E
|
||||
- `mem:backend/core` — JVM clojure.test conventions
|
||||
|
||||
## Key Rules
|
||||
|
||||
- Every behavior change needs a test
|
||||
- Test state, not interactions
|
||||
- DAMP over DRY — tests are specifications; duplication is OK if each test is self-contained and readable
|
||||
- Prefer Real > Fake > Stub > Mock
|
||||
- Arrange-Act-Assert structure
|
||||
- One assertion per concept
|
||||
- Never pipe test output to filters — redirect to file first
|
||||
- Register new test files in the module's runner/entrypoint
|
||||
|
||||
## Verification
|
||||
|
||||
After completing implementation:
|
||||
|
||||
- [ ] Every new behavior has a test
|
||||
- [ ] All tests pass for touched modules
|
||||
- [ ] Bug fixes include a reproduction test
|
||||
- [ ] Lint/formatter passes
|
||||
@ -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,13 +36,13 @@ 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):**
|
||||
@ -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']
|
||||
@ -415,98 +350,42 @@ if closed:
|
||||
### 11. Generate anomaly report and save to CHANGES-ISSUES.md
|
||||
|
||||
After all edits and cross-referencing are complete, generate a structured
|
||||
report and save it to `CHANGES-ISSUES.md` (overwriting if exists).
|
||||
anomaly 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:
|
||||
|
||||
```bash
|
||||
python3 << 'PYEOF'
|
||||
import json, re, subprocess, sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
MILESTONE = "<MILESTONE>"
|
||||
CHANGES_MD = "CHANGES.md"
|
||||
OUTPUT = "CHANGES-ISSUES.md"
|
||||
REPO = "penpot/penpot"
|
||||
|
||||
# --- 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 ---
|
||||
# Fetch milestone issues (all states)
|
||||
result = subprocess.run(
|
||||
["python3", "scripts/gh.py", "issues", MILESTONE, "--state", "all"],
|
||||
["python3", "tools/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}
|
||||
|
||||
# Fetch milestone PRs (all states)
|
||||
result = subprocess.run(
|
||||
["python3", "scripts/gh.py", "prs", "--milestone", MILESTONE, "--state", "all"],
|
||||
["python3", "tools/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 ---
|
||||
# Read changelog
|
||||
with open(CHANGES_MD) as f:
|
||||
content = f.read()
|
||||
|
||||
m = re.search(rf'## {re.escape(MILESTONE)}(?:\s*\([^)]*\))?\n(.*?)(?:\n## |\Z)', content, re.DOTALL)
|
||||
m = re.search(rf'## {MILESTONE} \(Unreleased\)\n(.*?)(?:\n## |\Z)', content, re.DOTALL)
|
||||
section = m.group(1) if m else ""
|
||||
|
||||
# Collect issue and PR references from the changelog section
|
||||
changelog_issues = set()
|
||||
for num in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/issues/\d+\)', section):
|
||||
changelog_issues.add(int(num))
|
||||
@ -519,159 +398,166 @@ for num in re.findall(r'\[#(\d+)\]\(https://github\.com/penpot/penpot/pull/\d+\)
|
||||
for num in re.findall(r'PR:\[(\d+)\]', section):
|
||||
changelog_prs.add(int(num))
|
||||
|
||||
# --- 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}
|
||||
|
||||
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]
|
||||
|
||||
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) ---
|
||||
# Determine valid (non-excluded) milestone issues
|
||||
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
|
||||
valid_issues = []
|
||||
for issue in all_issues:
|
||||
labels = set(issue.get('labels', []))
|
||||
if issue.get('state') != 'CLOSED': continue
|
||||
if issue.get('issue_type') in EXCLUDED_ISSUE_TYPES: continue
|
||||
if issue.get('project_status') in EXCLUDED_PROJECT_STATUS: continue
|
||||
if EXCLUDED_LABELS & labels: continue
|
||||
valid_issues.append(issue)
|
||||
valid_nums = {i['number'] for i in valid_issues}
|
||||
|
||||
# --- 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.
|
||||
# --- Gather anomalies ---
|
||||
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
|
||||
# Type 1: Entries in changelog that should be excluded
|
||||
for num in sorted(changelog_issues):
|
||||
issue = issue_by_num.get(num)
|
||||
if issue is None:
|
||||
anomalies.append({
|
||||
'type': 'should_remove',
|
||||
'severity': 'HIGH',
|
||||
'number': num,
|
||||
'title': '',
|
||||
'reason': 'Issue not found in milestone (deleted or moved)'
|
||||
})
|
||||
continue
|
||||
labels = set(issue.get('labels', []))
|
||||
reasons = []
|
||||
if issue.get('state') != 'CLOSED':
|
||||
reasons.append(f'state is "{issue["state"]}" (should be CLOSED)')
|
||||
if 'release blocker' in labels:
|
||||
reasons.append('has "release blocker" label')
|
||||
if 'no changelog' in labels:
|
||||
reasons.append('has "no changelog" label')
|
||||
if issue.get('issue_type') == 'Task':
|
||||
reasons.append(f'issue_type is Task (internal chore)')
|
||||
if issue.get('project_status') == 'Rejected':
|
||||
reasons.append('project_status is Rejected')
|
||||
if reasons:
|
||||
anomalies.append({
|
||||
'type': 'should_remove',
|
||||
'severity': 'MEDIUM' if issue.get('issue_type') == 'Task' else 'HIGH',
|
||||
'number': num,
|
||||
'title': issue.get('title', '')[:80],
|
||||
'reason': '; '.join(reasons)
|
||||
})
|
||||
|
||||
# Type 2: Valid issues not in changelog
|
||||
for num in sorted(valid_nums - changelog_issues):
|
||||
issue = issue_by_num[num]
|
||||
info = {
|
||||
'type': 'missing',
|
||||
'severity': 'MEDIUM',
|
||||
'number': num,
|
||||
'title': issue['title'][:80],
|
||||
'issue_type': issue['issue_type'],
|
||||
'closing_prs': issue.get('closing_prs', []),
|
||||
'note': ''
|
||||
}
|
||||
# Check for duplicate (same PR as existing entry)
|
||||
existing = []
|
||||
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
|
||||
})
|
||||
for cl_num in changelog_issues:
|
||||
cl_issue = issue_by_num.get(cl_num)
|
||||
if cl_issue and pr_num in cl_issue.get('closing_prs', []):
|
||||
existing.append(f'#{cl_num}')
|
||||
if existing:
|
||||
info['note'] = f'DUPLICATE: same PR as existing entry(ies): {", ".join(existing)}'
|
||||
# Check closing PRs not merged
|
||||
unmerged = []
|
||||
for pr_num in issue.get('closing_prs', []):
|
||||
pr = pr_by_num.get(pr_num)
|
||||
if pr is None:
|
||||
unmerged.append(f'#{pr_num} (unknown)')
|
||||
elif pr.get('state') != 'MERGED':
|
||||
unmerged.append(f'#{pr_num} (state={pr["state"]})')
|
||||
if unmerged:
|
||||
info['note'] = (info['note'] + '; ' if info['note'] else '') + f'Closing PRs not merged: {", ".join(unmerged)}'
|
||||
anomalies.append(info)
|
||||
|
||||
# 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}
|
||||
# Type 3: PRs in changelog that are not merged
|
||||
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_"
|
||||
if pr is None:
|
||||
anomalies.append({
|
||||
'type': 'unmerged_pr',
|
||||
'severity': 'HIGH',
|
||||
'number': pr_num,
|
||||
'title': '',
|
||||
'reason': 'PR not found in milestone PR list'
|
||||
})
|
||||
elif pr.get('state') != 'MERGED':
|
||||
anomalies.append({
|
||||
'type': 'unmerged_pr',
|
||||
'severity': 'HIGH',
|
||||
'number': pr_num,
|
||||
'title': pr.get('title', '')[:80],
|
||||
'reason': f'state={pr["state"]} (should be MERGED)'
|
||||
})
|
||||
|
||||
# --- Write report to CHANGES-ISSUES.md ---
|
||||
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(f'Generated: {__import__("datetime").datetime.now().strftime("%Y-%m-%d %H:%M UTC")}\n\n')
|
||||
f.write('---\n\n')
|
||||
|
||||
n_a = len(anomalies_a)
|
||||
n_b = len(anomalies_b)
|
||||
# Summary
|
||||
n_remove = sum(1 for a in anomalies if a['type'] == 'should_remove')
|
||||
n_missing = sum(1 for a in anomalies if a['type'] == 'missing')
|
||||
n_pr = sum(1 for a in anomalies if a['type'] == 'unmerged_pr')
|
||||
f.write(f'## Summary\n\n')
|
||||
f.write(f'- **Issues to remove from changelog:** {n_remove}\n')
|
||||
f.write(f'- **Valid issues missing from changelog:** {n_missing}\n')
|
||||
f.write(f'- **Unmerged PRs referenced:** {n_pr}\n')
|
||||
f.write(f'- **Total anomalies:** {len(anomalies)}\n\n')
|
||||
|
||||
f.write('## Summary\n\n')
|
||||
f.write(f'- **Issue in {MILESTONE}, referenced PR in different milestone or no milestone:** {n_a}\n')
|
||||
f.write(f'- **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')
|
||||
if not anomalies:
|
||||
f.write('✅ No anomalies found. The changelog is fully consistent with the milestone.\n\n')
|
||||
else:
|
||||
f.write('✅ No anomalies found. All (issue, PR) pairs in the changelog have aligned milestone assignments.\n\n')
|
||||
# Type 1
|
||||
if n_remove:
|
||||
f.write(f'## Issues to Remove\n\n')
|
||||
f.write('These entries are in the changelog but should be excluded based on current issue metadata.\n\n')
|
||||
for a in anomalies:
|
||||
if a['type'] != 'should_remove': continue
|
||||
badge = '🔴' if a['severity'] == 'HIGH' else '🟡'
|
||||
f.write(f'{badge} **#{a["number"]}**')
|
||||
if a.get('title'): f.write(f' — {a["title"]}')
|
||||
f.write(f'\n - Reason: {a["reason"]}\n\n')
|
||||
|
||||
# --- Context ---
|
||||
# Type 2
|
||||
if n_missing:
|
||||
f.write(f'## Valid Issues Not in Changelog\n\n')
|
||||
f.write('These issues are closed, non-excluded milestone items that lack a changelog entry.\n\n')
|
||||
for a in anomalies:
|
||||
if a['type'] != 'missing': continue
|
||||
f.write(f'❓ **#{a["number"]}** — {a["title"]}\n')
|
||||
f.write(f' - Type: {a["issue_type"]}, Closing PRs: {a["closing_prs"]}\n')
|
||||
if a.get('note'): f.write(f' - Note: {a["note"]}\n')
|
||||
f.write('\n')
|
||||
|
||||
# Type 3
|
||||
if n_pr:
|
||||
f.write(f'## Unmerged PRs Referenced in Changelog\n\n')
|
||||
f.write('These PR numbers appear in the changelog but are not merged.\n\n')
|
||||
for a in anomalies:
|
||||
if a['type'] != 'unmerged_pr': continue
|
||||
f.write(f'🔴 **#{a["number"]}**')
|
||||
if a.get('title'): f.write(f' — {a["title"]}')
|
||||
f.write(f'\n - {a["reason"]}\n\n')
|
||||
|
||||
# Appendix: counts
|
||||
f.write('---\n\n')
|
||||
f.write('## Context\n\n')
|
||||
f.write(f'- Milestone: **{MILESTONE}**\n')
|
||||
f.write(f'## Context\n\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'- Valid issues after exclusions: {len(valid_issues)}\n')
|
||||
f.write(f'- Issues referenced in changelog: {len(changelog_issues)}\n')
|
||||
f.write(f'- PRs referenced in changelog: {len(changelog_prs)}\n')
|
||||
|
||||
@ -679,24 +565,15 @@ print(f"Anomaly report written to {OUTPUT}")
|
||||
PYEOF
|
||||
```
|
||||
|
||||
This generates `CHANGES-ISSUES.md` containing **only the anomalies** —
|
||||
milestone mismatches between issues and their referenced PRs:
|
||||
|
||||
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.
|
||||
|
||||
**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.
|
||||
This generates `CHANGES-ISSUES.md` with three sections:
|
||||
1. **Issues to Remove** — Entries in the changelog that should be excluded based
|
||||
on current issue metadata (labels, type, project status, or deletion).
|
||||
2. **Valid Issues Not in Changelog** — Closed, non-excluded milestone issues
|
||||
that lack a changelog entry (with notes on duplicates and unmerged closing PRs).
|
||||
3. **Unmerged PRs Referenced** — PRs in the changelog that are not merged.
|
||||
|
||||
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.
|
||||
state of the milestone and changelog.
|
||||
|
||||
## Key Principles
|
||||
|
||||
@ -734,7 +611,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 +622,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.
|
||||
|
||||
@ -5,8 +5,7 @@ Backend: JVM Clojure; Integrant; PostgreSQL; Redis/Valkey; RPC; HTTP; storage; m
|
||||
## Focused memories
|
||||
|
||||
- RPC, DB helpers, workers, cron: `mem:backend/rpc-db-worker-subtleties`
|
||||
- Storage abstraction, logical buckets, object lifecycle, deduplication, access, and garbage collection: `mem:backend/storage`.
|
||||
- HTTP sessions, config, media processing, and file data persistence: `mem:backend/http-storage-filedata-subtleties`.
|
||||
- HTTP sessions, config, storage, media, file data persistence: `mem:backend/http-storage-filedata-subtleties`
|
||||
- Auth flows, permission model, teams, projects, invitations, comments, webhooks, audit: `mem:backend/auth-permissions-product-domains`
|
||||
- Services, task-queue/Pub-Sub topology constraints -> `mem:prod-infra/core`.
|
||||
|
||||
@ -40,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
|
||||
@ -57,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
|
||||
@ -93,18 +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`.
|
||||
|
||||
@ -14,7 +14,10 @@
|
||||
|
||||
## Storage and media
|
||||
|
||||
- Storage abstraction, backend configuration, logical buckets, object lifecycle, deduplication, access rules, and garbage collection: `mem:backend/storage`.
|
||||
- Storage has a fixed valid bucket set. Backends are `:fs` and `:s3`; default backend comes from deprecated `assets-storage-backend` only when present, otherwise `objects-storage-backend`, defaulting to `:fs`.
|
||||
- `put-object!` creates the DB `storage_object` row before writing backend content. Backend writes happen only for newly created rows, so deduplication can skip object writes.
|
||||
- Deduplication only applies when requested, when the content can provide a hash, and when bucket metadata is present. Reads exclude soft-deleted storage rows.
|
||||
- `sto/resolve` can reuse the current DB connection via `::db/reuse-conn true`; preserve this in transaction-sensitive code.
|
||||
- SVG validation strips DOCTYPE and uses secure SAX parsing. Basic SVG info falls back to 100x100 dimensions when width/height/viewBox are missing.
|
||||
- Raster metadata is shell-derived with ImageMagick `identify`, verifies detected MIME against the supplied MIME, and swaps dimensions for EXIF orientations 6/8.
|
||||
- Remote image download requires 2xx status, `content-length`, a known MIME, and size under the configured maximum before writing the temp file; mismatched byte count is an internal error.
|
||||
@ -25,4 +28,4 @@
|
||||
- File data backends are `legacy-db`, `db`, and `storage`. The storage backend keeps encoded file data in storage bucket `file-data`; the DB row stores metadata with `storage-ref-id` and nil data.
|
||||
- `fdata/upsert!` touches any storage object referenced by incoming metadata before storing the new row/blob.
|
||||
- Pointer-map fragments are persisted separately as type `fragment`, and only modified pointer maps are written.
|
||||
- `fdata/realize` combines pointer realization and object-map realization. Use it before operations that need complete in-memory file data instead of pointer placeholders.
|
||||
- `fdata/realize` combines pointer realization and object-map realization. Use it before operations that need complete in-memory file data instead of pointer placeholders.
|
||||
@ -1,83 +0,0 @@
|
||||
# Backend Storage
|
||||
|
||||
## Abstraction
|
||||
|
||||
- `app.storage` stores binary objects.
|
||||
- Each object has a `storage_object` database row.
|
||||
- The row stores the UUID, size, backend, timestamps, and Transit metadata.
|
||||
- The backend stores the binary content.
|
||||
- Supported backends are `:fs` and `:s3`.
|
||||
- FS uses one root directory and a UUID-derived path.
|
||||
- S3 uses one configured bucket and an optional prefix.
|
||||
- A Penpot bucket is metadata. It is not an S3 bucket or a filesystem directory.
|
||||
- FS and S3 use the same UUID-derived object path. The bucket does not change the path.
|
||||
- `PENPOT_OBJECTS_STORAGE_*` configures the current object backend.
|
||||
- Deprecated asset-storage config keys remain supported for migration.
|
||||
- Database rows keep the backend name. Keep the legacy `:assets-fs` and `:assets-s3` aliases.
|
||||
|
||||
## Object Lifecycle
|
||||
|
||||
- `put-object!` creates the database row before it writes backend content.
|
||||
- Backend content is written only when the row is new.
|
||||
- A failed backend write can leave an unreferenced database row.
|
||||
- Callers often set `:touched-at` so garbage collection can remove such rows.
|
||||
- `get-object` excludes rows with `deleted_at`.
|
||||
- Existing object values can remain readable until physical deletion.
|
||||
- `:expired-at` blocks reads after the expiration time.
|
||||
- `del-object!` sets `deleted_at`. It does not remove backend content.
|
||||
- `storage-gc-deleted` removes the database row and backend content after the deletion delay.
|
||||
- `storage-gc-touched` finds references before it sets `deleted_at`.
|
||||
- `objects-gc` removes deleted domain rows and touches their storage object IDs.
|
||||
- Use `::db/reuse-conn true` with `sto/resolve` inside a database transaction.
|
||||
|
||||
## Deduplication
|
||||
|
||||
- Deduplication requires `::sto/deduplicate?`, a content hash, and bucket metadata.
|
||||
- The lookup matches hash, bucket, backend, and `deleted_at IS NULL`.
|
||||
- The lookup does not include file ID, profile ID, team ID, or organization ID.
|
||||
- Objects can therefore share content across users and files within one bucket.
|
||||
- Deleted objects are not reused.
|
||||
- `tempfile` objects never use deduplication, even when the caller requests it.
|
||||
- Use `sto/wrap-with-hash` when the caller already calculated the content hash.
|
||||
|
||||
## Bucket Rules
|
||||
|
||||
| Bucket | Content and references | Dedup | Direct `/assets/by-id` access | Cleanup |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `file-media-object` | Original file images and generated media thumbnails. References: `file_media_object.media_id` and `thumbnail_id`. | Yes | Public | Reference scan. |
|
||||
| `team-font-variant` | Font variants in `team_font_variant`. References: `woff1_file_id`, `woff2_file_id`, `otf_file_id`, and `ttf_file_id`. | Yes | Public | Reference scan. |
|
||||
| `file-object-thumbnail` | Frame and component thumbnails in `file_tagged_object_thumbnail.media_id`. | Yes | Public | Reference scan. |
|
||||
| `file-thumbnail` | File grid thumbnails in `file_thumbnail.media_id`. | Yes | Authentication required | Reference scan. |
|
||||
| `profile` | User and team profile photos. References: `profile.photo_id` and `team.photo_id`. | Yes | Authentication required | Reference scan. |
|
||||
| `organization` | Organization logos uploaded by the Nitrate management API. | Yes | Public | No reference scan. A touched object is deleted. |
|
||||
| `tempfile` | Export files, chunked-upload chunks, and temporary font downloads. | No | Authentication required | No reference scan. A touched object uses a two-hour deletion delay. |
|
||||
| `file-data` | Encoded file data when `file-data-backend` is `storage`. Reference metadata has `storage-ref-id`, `file-id`, and the `file_data` row ID. | Yes | Authentication required | Reference scan. |
|
||||
| `file-data-fragment` | Compatibility value for file-data fragments. The current backend has no dedicated producer for this bucket. | No current write semantics | Public | No touched-object collector case. |
|
||||
| `file-change` | Compatibility value for file changes. Current snapshots store data in `file_data`, not this bucket. | No current write semantics | Authentication required | No touched-object collector case. |
|
||||
|
||||
- The valid bucket set lives in `app.storage/valid-buckets`.
|
||||
- `file-media-object` is the default bucket for old rows without bucket metadata.
|
||||
- Do not assign a new bucket without adding its access and cleanup behavior.
|
||||
- The touched-object collector raises an internal error for an unknown bucket.
|
||||
- It supports `file-media-object`, `team-font-variant`, `file-object-thumbnail`, `file-thumbnail`, `profile`, `file-data`, `tempfile`, and `organization`.
|
||||
- It does not support `file-data-fragment` or `file-change`.
|
||||
|
||||
## Access Rules
|
||||
|
||||
- `app.http.assets` decides direct object authentication from the bucket.
|
||||
- Public buckets are `file-media-object`, `file-object-thumbnail`, `team-font-variant`, `file-data-fragment`, and `organization`.
|
||||
- Other valid buckets require a session or access-token profile ID.
|
||||
- File-media routes also require file read permission.
|
||||
- Non-public direct responses set `content-disposition: attachment`.
|
||||
- FS responses use `x-accel-redirect` for the configured asset path.
|
||||
- S3 responses use a presigned URL and an HTTP redirect.
|
||||
|
||||
## File Data
|
||||
|
||||
- `file-data-backend` accepts `legacy-db`, `db`, or `storage`.
|
||||
- `legacy-db` stores main data in `file.data` and snapshots in `file_change.data`.
|
||||
- `db` stores encoded data in `file_data.data`.
|
||||
- `storage` stores encoded data in storage subsystem with `file-data` bucket and keeps `data` nil in `file_data` table.
|
||||
- The `file_data.metadata.storage-ref-id` value points to the storage object.
|
||||
- `fdata/upsert!` touches a storage object from incoming metadata before it stores the new row.
|
||||
- File snapshots use `file_data` for snapshot data and `file_change` for snapshot metadata.
|
||||
@ -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.
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
## Stable namespace map
|
||||
|
||||
- `app.common.data` and `app.common.data.macros`: generic data helpers and performance macros that do not depend on Penpot domain entities.
|
||||
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.organization` contains organization schemas, `apply-organization`, and fail-closed organization/team permission rules (`allowed?`, `can-send-invitations?`).
|
||||
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.nitrate-permissions` contains shared fail-closed Nitrate organization/team permission rules.
|
||||
- `app.common.files.*`: file-level operations, shape tree helpers, change application, migrations, validation, and undo/redo-related logic.
|
||||
- `app.common.logic.*`: higher-level workflows/algorithms over files, shapes, components, variants, libraries, tokens, etc.
|
||||
- `app.common.geom.*`: geometry helpers and transformations.
|
||||
@ -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
|
||||
|
||||
@ -50,4 +45,4 @@ For geometry-sensitive tests, read `mem:common/geometry-invariants` before posit
|
||||
|
||||
## Debugging
|
||||
|
||||
Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes.
|
||||
Use `mem:common/component-debugging-recipes` for shape-tree dumps, undo/change inspection, and temporary live instrumentation recipes.
|
||||
@ -6,40 +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`. Issue creation (titles, labels, body templates, Issue Types): `mem:workflow/creating-issues`.
|
||||
- You have access to the GitHub CLI `gh` or corresponding MCP tools.
|
||||
- Issues are also managed on Taiga. Read issues using the `read_taiga_issue` tool.
|
||||
- Before writing code, analyze the task in depth and describe your plan. If the task is complex, break it down into atomic steps.
|
||||
*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`.
|
||||
- `media-processor/`: TypeScript/Node.js HTTP service for image (sharp) and font (FontForge) processing; core conventions: `mem:media-processor/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`
|
||||
@ -53,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,9 +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. 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`.
|
||||
|
||||
Each workspace is independent and can be started/stopped in any order. Shared infra (postgres, minio, etc.) is shut down only when no instances remain running.
|
||||
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
|
||||
|
||||
@ -46,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.
|
||||
@ -65,8 +63,8 @@ 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).
|
||||
- `stop-devenv [--ws main|0|wsN|N] [--all]`: stop instances. Flags mutually exclusive. `--ws N` stops just that workspace. `--ws 0` or no flag stops ws0; shared infra shuts down only if no other instances remain. `--all` stops every ws highest-first then ws0, then infra.
|
||||
- `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.
|
||||
- `run-devenv-shell [--instance 0|wsN|N] [cmd...]`: bash in target instance. (`--instance` flag not yet renamed to `--ws`.)
|
||||
|
||||
@ -5,10 +5,8 @@
|
||||
## Layout and commands
|
||||
|
||||
- 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`; test bundle `pnpm run build:test`; tests `pnpm run test` or `pnpm run test:quiet`; lint `pnpm run lint:clj`; format check/fix `pnpm run check-fmt:clj` / `pnpm run fmt:clj`.
|
||||
- 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`.
|
||||
- Exporter test conventions and CI: `mem:exporter/testing`.
|
||||
|
||||
## HTTP and browser pool
|
||||
|
||||
@ -32,4 +30,4 @@
|
||||
- WebP is produced by taking a PNG screenshot and converting it with ImageMagick.
|
||||
- SVG export rasterizes text foreignObjects to PNG, converts through PPM/color masks/potrace, and reassembles SVG paths. It also replaces non-breaking spaces for SVG compatibility and drops empty defs/paths.
|
||||
- PDF export injects `@page` sizing through raw browser `evaluate` JavaScript; that code cannot rely on CLJS runtime helpers.
|
||||
- Temporary resources schedule local deletion, then uploads POST to `/api/management/methods/upload-tempfile` with `X-Shared-Key: exporter <management-key>` and Bearer auth.
|
||||
- Temporary resources schedule local deletion, then uploads POST to `/api/management/methods/upload-tempfile` with `X-Shared-Key: exporter <management-key>` and Bearer auth.
|
||||
@ -1,16 +0,0 @@
|
||||
# Exporter Testing
|
||||
|
||||
- READ `mem:testing` first.
|
||||
- Tests use `cljs.test` and live under `exporter/test/exporter_tests/`.
|
||||
- Register every test namespace in `exporter-tests.runner`.
|
||||
- From `exporter/`: `pnpm run build:test` builds the Node test bundle without running tests.
|
||||
- From `exporter/`: `pnpm run test` builds and runs tests with full output.
|
||||
- From `exporter/`: `pnpm run test:quiet` builds and runs tests with reduced build output.
|
||||
- After `build:test`, reuse the compiled bundle with `node target/tests/test.js`.
|
||||
- For iterative focused runs, build once and reuse the compiled bundle.
|
||||
- Focus a test namespace with `node target/tests/test.js --focus exporter-tests.renderer-svg-test`.
|
||||
- Focus a test var with `node target/tests/test.js --focus exporter-tests.renderer-svg-test/creates-the-correct-gradient-element`.
|
||||
- Set app log level by appending `--log-level warn` (or `trace|debug|info|warn|error`).
|
||||
- `test:quiet` accepts forwarded options but rebuilds the bundle; prefer the direct runner after `build:test` for focused runs.
|
||||
- From `exporter/`: `pnpm run check-fmt:clj` checks ClojureScript formatting.
|
||||
- From `exporter/`: `pnpm run lint:clj` runs ClojureScript linting.
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
# Media Processor
|
||||
|
||||
Stateless HTTP service for Penpot image and font processing. Handles image info extraction, thumbnail generation (sharp), and font conversion (FontForge, woff-tools).
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- Language: TypeScript
|
||||
- Runtime: Node.js
|
||||
- Framework: Express
|
||||
- Image processing: sharp (libvips)
|
||||
- Font processing: FontForge (TTF/OTF), sfnt2woff, woff2_decompress
|
||||
- Upload handling: multer (hybrid storage: memory for small, disk for large)
|
||||
- Logging: pino (with optional Loki transport)
|
||||
- Config validation: Zod
|
||||
- Testing: Vitest
|
||||
- Package Manager: pnpm
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
media-processor/
|
||||
├── src/
|
||||
│ ├── index.ts # Express app setup, routes, middleware
|
||||
│ ├── config.ts # Zod-validated env config, HKDF key derivation
|
||||
│ ├── types.ts # TypeScript type definitions
|
||||
│ ├── upload.ts # Multer configuration, getFileBuffer helper
|
||||
│ ├── upload-storage.ts # Hybrid storage engine (memory < threshold, disk >= threshold)
|
||||
│ ├── logger.ts # Pino logger setup
|
||||
│ ├── middleware/
|
||||
│ │ ├── auth.ts # Timing-safe shared key authentication
|
||||
│ │ ├── error-handler.ts # ProcessingError class, centralized error handling
|
||||
│ │ └── timeout.ts # Request timeout middleware
|
||||
│ ├── routes/
|
||||
│ │ ├── health.ts # GET /api/health
|
||||
│ │ ├── image.ts # POST /api/image/info, /api/image/thumbnail
|
||||
│ │ └── font.ts # POST /api/font/convert
|
||||
│ └── services/
|
||||
│ ├── image.ts # sharp-based image info/thumbnail generation
|
||||
│ ├── font.ts # FontForge/woff-tools font conversion
|
||||
│ └── errors.ts # throwValidation, throwRestriction, throwProcessing
|
||||
├── test/ # Vitest test files
|
||||
├── vitest.config.ts # Test configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── esbuild.config.mjs # Build configuration
|
||||
└── package.json # Dependencies and scripts
|
||||
```
|
||||
|
||||
## Key Conventions
|
||||
|
||||
### Auth
|
||||
- Requests authenticated via `x-shared-key` header using timing-safe comparison
|
||||
- When no key configured, all requests rejected with 403
|
||||
- Key derived from `PENPOT_SECRET_KEY` via HKDF (blake2b512) or set directly via `PENPOT_MEDIA_PROCESSOR_SHARED_KEY`
|
||||
|
||||
### Resource Limits
|
||||
- Image: max pixels, max width/height enforced before processing
|
||||
- Font: prlimit wraps FontForge processes with memory (AS) and CPU time limits
|
||||
- Concurrency: p-queue limits concurrent requests (default 10)
|
||||
- Upload: hybrid storage — memory for files < 10MB, disk for larger; configurable via `PENPOT_MEDIA_PROCESSOR_MEMORY_THRESHOLD`
|
||||
- Max file size: configurable (default 350MB)
|
||||
|
||||
### Error Handling
|
||||
- `throwValidation(code, hint)` — 400 errors for invalid input
|
||||
- `throwRestriction(code, hint)` — 413 errors for resource limits exceeded
|
||||
- `throwProcessing(code, hint)` — 503 errors for processing failures (e.g., resource limit kills)
|
||||
|
||||
### Image Processing
|
||||
- EXIF orientation applied before dimension validation and thumbnail generation
|
||||
- sharp caching disabled to prevent unbounded memory growth
|
||||
- `withoutEnlargement: true` prevents upscaling small images
|
||||
|
||||
### Font Conversion
|
||||
- Supported formats: TTF, OTF, WOFF, WOFF2
|
||||
- SFNT type detected via magic bytes (0x4f54544f = OTF, 0x00010000 = TTF)
|
||||
- Temp files cleaned up in finally blocks (best-effort)
|
||||
|
||||
## Commands
|
||||
|
||||
All commands run from `media-processor/` directory:
|
||||
|
||||
- `pnpm run test` — Run Vitest test suite
|
||||
- `pnpm run types:check` — TypeScript type checking (tsc --noEmit)
|
||||
- `pnpm run fmt` — Format code with Prettier
|
||||
- `pnpm run fmt:check` — Check formatting without modifying
|
||||
- `pnpm run build` — Build for production (esbuild)
|
||||
- `pnpm run start:dev` — Start development server (tsx)
|
||||
|
||||
## Docker
|
||||
|
||||
- Exposed port: 6065 (configurable via `PENPOT_MEDIA_PROCESSOR_PORT`)
|
||||
- Must be deployed on internal Docker network only (not public-facing)
|
||||
- Backend communicates via `PENPOT_MEDIA_PROCESSING_SERVICE_URI`
|
||||
|
||||
## Testing Principles
|
||||
|
||||
Cross-cutting testing principles and anti-patterns: `mem:testing`.
|
||||
|
||||
- Run `pnpm run test` after changes
|
||||
- Run `pnpm run types:check` after TypeScript changes
|
||||
- Run `pnpm run fmt:check` before commits
|
||||
@ -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
|
||||
|
||||
@ -6,7 +6,7 @@ Backend (`app.config`, `PENPOT_*` env vars) is parameterized; deployments choose
|
||||
|
||||
- **PostgreSQL**: durable store. Profiles, teams, files, sessions, audit, `storage_object` metadata, the `task` queue, `scheduled_task` cron registry, migrations. File-data also lives here when the file-data backend is `legacy-db`/`db`. One shared DB across all backends.
|
||||
- **Redis (Valkey-compatible)**: per-backend message bus and cache. Concrete uses: msgbus Pub/Sub for collaborative-editing broadcasts and team/profile-org notifications fired by RPC handlers (`app.rpc.notifications`, `files_update`, `teams`, `websocket`); file-summary cache gated by `enable-redis-cache`; rate-limit counters; and the dispatcher→runner work hand-off list `penpot.worker.queue:<tenant>:<queue>`. `PENPOT_REDIS_URI`.
|
||||
- **Object storage**: backends `:s3` and `:fs`. S3 in prod; devenv uses MinIO. Holds uploaded media, file-data when the file-data backend is `storage`, exports. Backend-side details (resolve, dedup, bucket set, object lifecycle, and file-data backends): `mem:backend/storage`.
|
||||
- **Object storage**: backends `:s3` and `:fs`. S3 in prod; devenv uses MinIO. Holds uploaded media, file-data when the file-data backend is `storage`, exports. Backend-side details (resolve, dedup, bucket set, file-data backends): `mem:backend/http-storage-filedata-subtleties`.
|
||||
- **SMTP mailer**: invitations, password resets, email verification (sent via the `:sendmail` worker task).
|
||||
- **LDAP** (optional auth provider): helpers in `app.auth.*`, gated by `enable-login-with-ldap`.
|
||||
|
||||
@ -30,4 +30,4 @@ Penpot in production lives with both: horizontal-scale deployments accept "exact
|
||||
## See also
|
||||
|
||||
- Devenv composition and the ws0-only worker placement: `mem:devenv/core`.
|
||||
- Storage backend resolution, dedup, bucket behavior, object lifecycle, and file-data lifecycle: `mem:backend/storage`.
|
||||
- Storage backend resolution, dedup, file-data lifecycle: `mem:backend/http-storage-filedata-subtleties`.
|
||||
|
||||
@ -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`.
|
||||
|
||||
|
||||
@ -17,16 +17,9 @@
|
||||
|
||||
## Tile/render behavior
|
||||
|
||||
- Raster `Fill::Image`: skip `save_layer` unless the shape has an image filter; plain
|
||||
Rect/Frame (no corners) also skip the container clip (`draw_image_fill` in fills.rs).
|
||||
- Zoom settle: visible tiles present via `FrameType::ViewportReady` before interest-ring
|
||||
work; crop-cache rebuild is deferred to the later `Full` so the soft→sharp snap is
|
||||
compose+present only.
|
||||
- Interactive transforms are distinct from viewport fast mode. `set_modifiers_start` enables fast mode and interactive transform; interactive transform still flushes each animation frame.
|
||||
- During interactive transform, modifier tile invalidation is deferred to `render()` once per rAF. Outside interactive transform, `set_modifiers` rebuilds modifier tiles immediately.
|
||||
- `set_modifiers_end` disables fast/interactive state and cancels pending async render; the caller must request the final full-quality render.
|
||||
- Plain viewport fast mode (`options.is_viewport_interaction()`) renders from cache and does not flush target output inside `process_animation_frame`; interactive transforms do flush.
|
||||
- Zoom changes rebuild the tile index while preserving cached tile textures. Avoid replacing that path with shallow rebuilds if blur/shadow cache preservation matters.
|
||||
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
|
||||
- Frames with a fill may use `render_frame_container_drop_shadow` (direct rrect +
|
||||
blur saveLayer on `DropShadows`) when `uses_direct_container_drop_shadow` is true.
|
||||
- Pending tile priority is intentionally reversed by pop order; check the queue construction before changing tile scheduling.
|
||||
@ -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,105 +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.
|
||||
- Listing or inspecting GitHub Security Advisories (GHSA).
|
||||
|
||||
## 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.
|
||||
|
||||
### `advisories`
|
||||
|
||||
List or inspect GitHub Security Advisories for the repository.
|
||||
|
||||
```bash
|
||||
# List all advisories (summary view)
|
||||
python3 scripts/gh.py advisories
|
||||
|
||||
# Filter by severity
|
||||
python3 scripts/gh.py advisories --severity critical
|
||||
|
||||
# Filter by state
|
||||
python3 scripts/gh.py advisories --state triage
|
||||
|
||||
# Get full detail for a single advisory
|
||||
python3 scripts/gh.py advisories GHSA-xvj6-fh9w-gjw7
|
||||
```
|
||||
|
||||
**Summary output fields**: ghsa_id, cve_id, severity, cvss_score, state, summary, cwes, published_at, closed_at, url.
|
||||
|
||||
**Detail output** (single advisory) adds: description, vulnerabilities (package, version ranges), credits, timestamps.
|
||||
|
||||
**Output**: JSON 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)
|
||||
@ -14,20 +14,10 @@ automatically pull the identity from the local git config `user.name` and `user.
|
||||
:emoji: Subject line (imperative, capitalized, no period, <=70 chars)
|
||||
|
||||
Body explaining what changed and why.
|
||||
Wrap lines at 72 characters — git log and tooling
|
||||
render long lines poorly. Keep each line concise.
|
||||
|
||||
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
|
||||
|
||||
## Referencing Issues
|
||||
|
||||
Use `Closes #NNNN` (not `Fixes #NNNN`) to link a commit to a GitHub issue.
|
||||
|
||||
@ -35,7 +35,7 @@ Command what should be built. Format: `[Imperative verb] [what] in/on [where]`.
|
||||
|
||||
| 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) |
|
||||
| **Labels** | `bug` (crashes/regressions) · `enhancement` (new features) · `community contribution` (PRs from non-core) · skip workflow labels (`backport candidate`, `team-qa`) |
|
||||
| **Milestone** | Use the current or next planned milestone. Fetch available milestones: `gh api repos/penpot/penpot/milestones --jq '.[].title'`. If unsure, omit. |
|
||||
| **Project** | Always `Main` (project number 8). Use `--project "Main"` flag. |
|
||||
| **Issue Type** | See Issue Type section below. Cannot be set via `gh issue create` — use GraphQL after creation. |
|
||||
@ -79,8 +79,6 @@ Write the body to a temp file to avoid shell quoting issues:
|
||||
<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
|
||||
@ -114,8 +112,8 @@ Output: `https://github.com/penpot/penpot/issues/<NUMBER>`
|
||||
| Docs | `IT_kwDOAcyBPM4B_IQz` |
|
||||
|
||||
**Map:**
|
||||
- Bug report (steps to reproduce, expected vs. actual) → Bug
|
||||
- Enhancement / new feature → Enhancement
|
||||
- `bug` label → Bug
|
||||
- `enhancement` label → Enhancement
|
||||
- Feature/epic → Feature
|
||||
- Docs → Docs
|
||||
- None of the above → Task
|
||||
@ -157,199 +155,6 @@ query { repository(owner: "penpot", name: "penpot") {
|
||||
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.
|
||||
- Creating issues **from PRs** (separating WHAT from HOW): `mem:workflow/creating-prs`
|
||||
|
||||
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