diff --git a/.gitignore b/.gitignore index 5d718c3363..6a23f2101f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,9 @@ /.clj-kondo/.cache /_dump /notes +/.opencode/package-lock.json +/plans +/prompts /playground/ /backend/*.md !/backend/AGENTS.md diff --git a/.opencode/agents/commiter.md b/.opencode/agents/commiter.md index 3020e5aac1..a0ac40e443 100644 --- a/.opencode/agents/commiter.md +++ b/.opencode/agents/commiter.md @@ -1,11 +1,13 @@ --- name: commiter description: Git commit assistant following CONTRIBUTING.md commit rules -mode: primary +mode: subagent --- -Role: You are responsible for creating git commits for Penpot and must follow -the repository commit-format rules exactly. +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: diff --git a/.opencode/agents/engineer.md b/.opencode/agents/engineer.md index b85205f031..b5ba1bf3f7 100644 --- a/.opencode/agents/engineer.md +++ b/.opencode/agents/engineer.md @@ -1,5 +1,5 @@ --- -name: engineer +name: Penpot Engineer description: Senior Full-Stack Software Engineer mode: primary --- diff --git a/.opencode/agents/planner.md b/.opencode/agents/planner.md new file mode 100644 index 0000000000..1236dd7458 --- /dev/null +++ b/.opencode/agents/planner.md @@ -0,0 +1,61 @@ +--- +name: Penpot Planner +description: Software architect for planning and analysis only +mode: primary +permission: + edit: ask +--- + +# Penpot Planner + +## 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. + +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` files (root and per-module) to understand structure and + conventions. +* Search code using `ripgrep` skill (`rg`) to trace dependencies, find patterns, + and understand existing implementations. +* 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-.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. + + diff --git a/.opencode/agents/prompt-assistant.md b/.opencode/agents/prompt-assistant.md new file mode 100644 index 0000000000..fb1f5bee8f --- /dev/null +++ b/.opencode/agents/prompt-assistant.md @@ -0,0 +1,59 @@ +--- +name: Prompt Assistant +description: Refines and improves prompts for maximum clarity and effectiveness +mode: all +--- + +# Prompt Assistant + +## 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 the root `AGENTS.md` to understand the repository and application + architecture. Then read the `AGENTS.md` **only** for each affected module. +* 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-.md for future use. diff --git a/.opencode/agents/testing.md b/.opencode/agents/testing.md deleted file mode 100644 index 17c19aade1..0000000000 --- a/.opencode/agents/testing.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: testing -description: Senior Software Engineer specialized on testing -mode: primary ---- - -Role: You are a Senior Software Engineer specialized in testing Clojure and -ClojureScript codebases. You work on Penpot, an open-source design tool. - -Tech stack: Clojure (backend/JVM), ClojureScript (frontend/Node.js), shared -Cljc (common module), Rust (render-wasm). - -Requirements: - -* Read the root `AGENTS.md` to understand the repository and application - architecture. Then read the `AGENTS.md` **only** for each affected module. Not all - modules have one — verify before reading. -* Before writing code, describe your plan. If the task is complex, break it down into - atomic steps. -* Tests should be exhaustive and include edge cases relevant to Penpot's domain: - nil/missing fields, empty collections, invalid UUIDs, boundary geometries, Malli schema - violations, concurrent state mutations, and timeouts. -* Tests must be deterministic — do not use `setTimeout`, real network calls, or rely on - execution order. Use synchronous mocks for asynchronous workflows. -* Use `with-redefs` or equivalent mocking utilities to isolate the logic under test. Avoid - testing through the UI (DOM); e2e tests cover that. -* Only reference functions, namespaces, or test utilities that actually exist in the - codebase. Verify their existence before citing them. -* After adding or modifying tests, run the applicable lint and format checks for the - affected module before considering the work done (see module `AGENTS.md` for exact - commands). -* Make small and logical commits following the commit guideline described in - `CONTRIBUTING.md`. Commit only when explicitly asked. -- 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. Allow git commit to - automatically pull the identity from the local git config `user.name` and `user.email`. diff --git a/.opencode/skills/bat-cat/SKILL.md b/.opencode/skills/bat-cat/SKILL.md new file mode 100644 index 0000000000..61ca8def8f --- /dev/null +++ b/.opencode/skills/bat-cat/SKILL.md @@ -0,0 +1,210 @@ +--- +name: bat-cat +description: A cat clone with syntax highlighting, line numbers, and Git integration - a modern replacement for cat. +homepage: https://github.com/sharkdp/bat +metadata: {"clawdbot":{"emoji":"🦇","requires":{"bins":["bat"]},"install":[{"id":"brew","kind":"brew","formula":"bat","bins":["bat"],"label":"Install bat (brew)"},{"id":"apt","kind":"apt","package":"bat","bins":["bat"],"label":"Install bat (apt)"}]}} +--- + +# bat - Better cat + +`cat` with syntax highlighting, line numbers, and Git integration. + +## Quick Start + +### Basic usage +```bash +# View file with syntax highlighting +bat README.md + +# Multiple files +bat file1.js file2.py + +# With line numbers (default) +bat script.sh + +# Without line numbers +bat -p script.sh +``` + +### Viewing modes +```bash +# Plain mode (like cat) +bat -p file.txt + +# Show non-printable characters +bat -A file.txt + +# Squeeze blank lines +bat -s file.txt + +# Paging (auto for large files) +bat --paging=always file.txt +bat --paging=never file.txt +``` + +## Syntax Highlighting + +### Language detection +```bash +# Auto-detect from extension +bat script.py + +# Force specific language +bat -l javascript config.txt + +# Show all languages +bat --list-languages +``` + +### Themes +```bash +# List available themes +bat --list-themes + +# Use specific theme +bat --theme="Monokai Extended" file.py + +# Set default theme in config +# ~/.config/bat/config: --theme="Dracula" +``` + +## Line Ranges + +```bash +# Show specific lines +bat -r 10:20 file.txt + +# From line to end +bat -r 100: file.txt + +# Start to specific line +bat -r :50 file.txt + +# Multiple ranges +bat -r 1:10 -r 50:60 file.txt +``` + +## Git Integration + +```bash +# Show Git modifications (added/removed/modified lines) +bat --diff file.txt + +# Show decorations (Git + file header) +bat --decorations=always file.txt +``` + +## Output Control + +```bash +# Output raw (no styling) +bat --style=plain file.txt + +# Customize style +bat --style=numbers,changes file.txt + +# Available styles: auto, full, plain, changes, header, grid, numbers, snip +bat --style=header,grid,numbers file.txt +``` + +## Common Use Cases + +**Quick file preview:** +```bash +bat file.json +``` + +**View logs with syntax highlighting:** +```bash +bat error.log +``` + +**Compare files visually:** +```bash +bat --diff file1.txt +bat file2.txt +``` + +**Preview before editing:** +```bash +bat config.yaml && vim config.yaml +``` + +**Cat replacement in pipes:** +```bash +bat -p file.txt | grep "pattern" +``` + +**View specific function:** +```bash +bat -r 45:67 script.py # If function is on lines 45-67 +``` + +## Integration with other tools + +**As pager for man pages:** +```bash +export MANPAGER="sh -c 'col -bx | bat -l man -p'" +man grep +``` + +**With ripgrep:** +```bash +rg "pattern" -l | xargs bat +``` + +**With fzf:** +```bash +fzf --preview 'bat --color=always --style=numbers {}' +``` + +**With diff:** +```bash +diff -u file1 file2 | bat -l diff +``` + +## Configuration + +Create `~/.config/bat/config` for defaults: + +``` +# Set theme +--theme="Dracula" + +# Show line numbers, Git modifications and file header, but no grid +--style="numbers,changes,header" + +# Use italic text on terminal +--italic-text=always + +# Add custom mapping +--map-syntax "*.conf:INI" +``` + +## Performance Tips + +- Use `-p` for plain mode when piping +- Use `--paging=never` when output is used programmatically +- `bat` caches parsed files for faster subsequent access + +## Tips + +- **Alias:** `alias cat='bat -p'` for drop-in cat replacement +- **Pager:** Use as pager with `export PAGER="bat"` +- **On Debian/Ubuntu:** Command may be `batcat` instead of `bat` +- **Custom syntaxes:** Add to `~/.config/bat/syntaxes/` +- **Performance:** For huge files, use `bat --paging=never` or plain `cat` + +## Common flags + +- `-p` / `--plain`: Plain mode (no line numbers/decorations) +- `-n` / `--number`: Only show line numbers +- `-A` / `--show-all`: Show non-printable characters +- `-l` / `--language`: Set language for syntax highlighting +- `-r` / `--line-range`: Only show specific line range(s) + +## Documentation + +GitHub: https://github.com/sharkdp/bat +Man page: `man bat` +Customization: https://github.com/sharkdp/bat#customization diff --git a/.opencode/skills/fd-find/SKILL.md b/.opencode/skills/fd-find/SKILL.md new file mode 100644 index 0000000000..e218ac9bfd --- /dev/null +++ b/.opencode/skills/fd-find/SKILL.md @@ -0,0 +1,194 @@ +--- +name: fd-find +description: A fast and user-friendly alternative to 'find' - simple syntax, smart defaults, respects gitignore. +homepage: https://github.com/sharkdp/fd +metadata: {"clawdbot":{"emoji":"📂","requires":{"bins":["fd"]},"install":[{"id":"brew","kind":"brew","formula":"fd","bins":["fd"],"label":"Install fd (brew)"},{"id":"apt","kind":"apt","package":"fd-find","bins":["fd"],"label":"Install fd (apt)"}]}} +--- + +# fd - Fast File Finder + +User-friendly alternative to `find` with smart defaults. + +## Quick Start + +### Basic search +```bash +# Find files by name +fd pattern + +# Find in specific directory +fd pattern /path/to/dir + +# Case-insensitive +fd -i pattern +``` + +### Common patterns +```bash +# Find all Python files +fd -e py + +# Find multiple extensions +fd -e py -e js -e ts + +# Find directories only +fd -t d pattern + +# Find files only +fd -t f pattern + +# Find symlinks +fd -t l +``` + +## Advanced Usage + +### Filtering +```bash +# Exclude patterns +fd pattern -E "node_modules" -E "*.min.js" + +# Include hidden files +fd -H pattern + +# Include ignored files (.gitignore) +fd -I pattern + +# Search all (hidden + ignored) +fd -H -I pattern + +# Maximum depth +fd pattern -d 3 +``` + +### Execution +```bash +# Execute command on results +fd -e jpg -x convert {} {.}.png + +# Parallel execution +fd -e md -x wc -l + +# Use with xargs +fd -e log -0 | xargs -0 rm +``` + +### Regex patterns +```bash +# Full regex search +fd '^test.*\.js$' + +# Match full path +fd --full-path 'src/.*/test' + +# Glob pattern +fd -g "*.{js,ts}" +``` + +## Time-based filtering +```bash +# Modified within last day +fd --changed-within 1d + +# Modified before specific date +fd --changed-before 2024-01-01 + +# Created recently +fd --changed-within 1h +``` + +## Size filtering +```bash +# Files larger than 10MB +fd --size +10m + +# Files smaller than 1KB +fd --size -1k + +# Specific size range +fd --size +100k --size -10m +``` + +## Output formatting +```bash +# Absolute paths +fd --absolute-path + +# List format (like ls -l) +fd --list-details + +# Null separator (for xargs) +fd -0 pattern + +# Color always/never/auto +fd --color always pattern +``` + +## Common Use Cases + +**Find and delete old files:** +```bash +fd --changed-before 30d -t f -x rm {} +``` + +**Find large files:** +```bash +fd --size +100m --list-details +``` + +**Copy all PDFs to directory:** +```bash +fd -e pdf -x cp {} /target/dir/ +``` + +**Count lines in all Python files:** +```bash +fd -e py -x wc -l | awk '{sum+=$1} END {print sum}' +``` + +**Find broken symlinks:** +```bash +fd -t l -x test -e {} \; -print +``` + +**Search in specific time window:** +```bash +fd --changed-within 2d --changed-before 1d +``` + +## Integration with other tools + +**With ripgrep:** +```bash +fd -e js | xargs rg "pattern" +``` + +**With fzf (fuzzy finder):** +```bash +vim $(fd -t f | fzf) +``` + +**With bat (cat alternative):** +```bash +fd -e md | xargs bat +``` + +## Performance Tips + +- `fd` is typically much faster than `find` +- Respects `.gitignore` by default (disable with `-I`) +- Uses parallel traversal automatically +- Smart case: lowercase = case-insensitive, any uppercase = case-sensitive + +## Tips + +- Use `-t` for type filtering (f=file, d=directory, l=symlink, x=executable) +- `-e` for extension is simpler than `-g "*.ext"` +- `{}` in `-x` commands represents the found path +- `{.}` strips the extension +- `{/}` gets basename, `{//}` gets directory + +## Documentation + +GitHub: https://github.com/sharkdp/fd +Man page: `man fd` diff --git a/.opencode/skills/jq-json-processor/SKILL.md b/.opencode/skills/jq-json-processor/SKILL.md new file mode 100644 index 0000000000..83fe48d7bf --- /dev/null +++ b/.opencode/skills/jq-json-processor/SKILL.md @@ -0,0 +1,112 @@ +--- +name: jq-json-processor +description: Process, filter, and transform JSON data using jq - the lightweight and flexible command-line JSON processor. +homepage: https://jqlang.github.io/jq/ +metadata: {"clawdbot":{"emoji":"🔍","requires":{"bins":["jq"]},"install":[{"id":"brew","kind":"brew","formula":"jq","bins":["jq"],"label":"Install jq (brew)"},{"id":"apt","kind":"apt","package":"jq","bins":["jq"],"label":"Install jq (apt)"}]}} +--- + +# jq JSON Processor + +Process, filter, and transform JSON data with jq. + +## Quick Examples + +### Basic filtering +```bash +# Extract a field +echo '{"name":"Alice","age":30}' | jq '.name' +# Output: "Alice" + +# Multiple fields +echo '{"name":"Alice","age":30}' | jq '{name: .name, age: .age}' + +# Array indexing +echo '[1,2,3,4,5]' | jq '.[2]' +# Output: 3 +``` + +### Working with arrays +```bash +# Map over array +echo '[{"name":"Alice"},{"name":"Bob"}]' | jq '.[].name' +# Output: "Alice" "Bob" + +# Filter array +echo '[1,2,3,4,5]' | jq 'map(select(. > 2))' +# Output: [3,4,5] + +# Length +echo '[1,2,3]' | jq 'length' +# Output: 3 +``` + +### Common operations +```bash +# Pretty print JSON +cat file.json | jq '.' + +# Compact output +cat file.json | jq -c '.' + +# Raw output (no quotes) +echo '{"name":"Alice"}' | jq -r '.name' +# Output: Alice + +# Sort keys +echo '{"z":1,"a":2}' | jq -S '.' +``` + +### Advanced filtering +```bash +# Select with conditions +jq '[.[] | select(.age > 25)]' people.json + +# Group by +jq 'group_by(.category)' items.json + +# Reduce +echo '[1,2,3,4,5]' | jq 'reduce .[] as $item (0; . + $item)' +# Output: 15 +``` + +### Working with files +```bash +# Read from file +jq '.users[0].name' users.json + +# Multiple files +jq -s '.[0] * .[1]' file1.json file2.json + +# Modify and save +jq '.version = "2.0"' package.json > package.json.tmp && mv package.json.tmp package.json +``` + +## Common Use Cases + +**Extract specific fields from API response:** +```bash +curl -s https://api.github.com/users/octocat | jq '{name: .name, repos: .public_repos, followers: .followers}' +``` + +**Convert CSV-like data:** +```bash +jq -r '.[] | [.name, .email, .age] | @csv' users.json +``` + +**Debug API responses:** +```bash +curl -s https://api.example.com/data | jq '.' +``` + +## Tips + +- Use `-r` for raw string output (removes quotes) +- Use `-c` for compact output (single line) +- Use `-S` to sort object keys +- Use `--arg name value` to pass variables +- Pipe multiple jq operations: `jq '.a' | jq '.b'` + +## Documentation + +Full manual: https://jqlang.github.io/jq/manual/ +Interactive tutorial: https://jqplay.org/ diff --git a/.opencode/skills/ripgrep/SKILL.md b/.opencode/skills/ripgrep/SKILL.md new file mode 100644 index 0000000000..31c3a83d5e --- /dev/null +++ b/.opencode/skills/ripgrep/SKILL.md @@ -0,0 +1,150 @@ +--- +name: ripgrep +description: Blazingly fast text search tool - recursively searches directories for regex patterns with respect to gitignore rules. +homepage: https://github.com/BurntSushi/ripgrep +metadata: {"clawdbot":{"emoji":"🔎","requires":{"bins":["rg"]},"install":[{"id":"brew","kind":"brew","formula":"ripgrep","bins":["rg"],"label":"Install ripgrep (brew)"},{"id":"apt","kind":"apt","package":"ripgrep","bins":["rg"],"label":"Install ripgrep (apt)"}]}} +--- + +# ripgrep (rg) + +Fast, smart recursive search. Respects `.gitignore` by default. + +## Quick Start + +### Basic search +```bash +# Search for "TODO" in current directory +rg "TODO" + +# Case-insensitive search +rg -i "fixme" + +# Search specific file types +rg "error" -t py # Python files only +rg "function" -t js # JavaScript files +``` + +### Common patterns +```bash +# Whole word match +rg -w "test" + +# Show only filenames +rg -l "pattern" + +# Show with context (3 lines before/after) +rg -C 3 "function" + +# Count matches +rg -c "import" +``` + +## Advanced Usage + +### File type filtering +```bash +# Multiple file types +rg "error" -t py -t js + +# Exclude file types +rg "TODO" -T md -T txt + +# List available types +rg --type-list +``` + +### Search modifiers +```bash +# Regex search +rg "user_\d+" + +# Fixed string (no regex) +rg -F "function()" + +# Multiline search +rg -U "start.*end" + +# Only show matches, not lines +rg -o "https?://[^\s]+" +``` + +### Path filtering +```bash +# Search specific directory +rg "pattern" src/ + +# Glob patterns +rg "error" -g "*.log" +rg "test" -g "!*.min.js" + +# Include hidden files +rg "secret" --hidden + +# Search all files (ignore .gitignore) +rg "pattern" --no-ignore +``` + +## Replacement Operations + +```bash +# Preview replacements +rg "old_name" --replace "new_name" + +# Actually replace (requires extra tool like sd) +rg "old_name" -l | xargs sed -i 's/old_name/new_name/g' +``` + +## Performance Tips + +```bash +# Parallel search (auto by default) +rg "pattern" -j 8 + +# Skip large files +rg "pattern" --max-filesize 10M + +# Memory map files +rg "pattern" --mmap +``` + +## Common Use Cases + +**Find TODOs in code:** +```bash +rg "TODO|FIXME|HACK" --type-add 'code:*.{rs,go,py,js,ts}' -t code +``` + +**Search in specific branches:** +```bash +git show branch:file | rg "pattern" +``` + +**Find files containing multiple patterns:** +```bash +rg "pattern1" | rg "pattern2" +``` + +**Search with context and color:** +```bash +rg -C 2 --color always "error" | less -R +``` + +## Comparison to grep + +- **Faster:** Typically 5-10x faster than grep +- **Smarter:** Respects `.gitignore`, skips binary files +- **Better defaults:** Recursive, colored output, line numbers +- **Easier:** Simpler syntax for common tasks + +## Tips + +- `rg` is often faster than `grep -r` +- Use `-t` for file type filtering instead of `--include` +- Combine with other tools: `rg pattern -l | xargs tool` +- Add custom types in `~/.ripgreprc` +- Use `--stats` to see search performance + +## Documentation + +GitHub: https://github.com/BurntSushi/ripgrep +User Guide: https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0e8f30e605..d733ea5c7a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,17 @@ Center](https://help.penpot.app/). - [Prerequisites](#prerequisites) - [Reporting Bugs](#reporting-bugs) - [Pull Requests](#pull-requests) + - [Workflow](#workflow) + - [Title format](#title-format) + - [Description](#description) + - [Branch naming](#branch-naming) + - [Review process](#review-process) + - [What we won't accept](#what-we-wont-accept) + - [Good first issues](#good-first-issues) - [Commit Guidelines](#commit-guidelines) + - [Commit types](#commit-types) + - [Rules](#rules) + - [Examples](#examples) - [Formatting and Linting](#formatting-and-linting) - [Changelog](#changelog) - [Code of Conduct](#code-of-conduct) @@ -52,15 +62,98 @@ Advisories](https://github.com/penpot/penpot/security/advisories) 1. **Read the DCO** — see [Developer's Certificate of Origin](#developers-certificate-of-origin-dco) below. All code patches must include a `Signed-off-by` line. -2. **Discuss before building** — open a question/discussion issue before - starting work on a new feature or significant change. No PR will be - accepted without prior discussion, whether it is a new feature, a planned - one, or a quick win. +2. **Discuss before building** — open a [GitHub + Issue](https://github.com/penpot/penpot/issues) or start a [GitHub + Discussion](https://github.com/penpot/penpot/discussions) before starting + work on a new feature or significant change. For planned features on the + roadmap, reference the corresponding Taiga story. No PR will be accepted + without prior discussion, whether it is a new feature, a planned one, or a + quick win. 3. **Bug fixes** — you may submit a PR directly, but we still recommend filing an issue first so we can track it independently of your fix. 4. **Format and lint** — run the checks described in [Formatting and Linting](#formatting-and-linting) before submitting. +### Title format + +Pull request titles **must** follow the same convention as commit subjects: + +``` +:emoji: +``` + +- Use the **imperative mood** (e.g. "Fix", not "Fixed"). +- Capitalize the first letter of the subject. +- Do not end the subject with a period. +- Keep the subject to **70 characters** or fewer. +- Use one of the [commit type emojis](#commit-types) listed below. + +When a PR contains multiple unrelated commits, choose the emoji that +best represents the dominant change. + +**Examples:** + +``` +:bug: Fix unexpected error on launching modal +:sparkles: Enable new modal for profile +:zap: Improve performance of dashboard navigation +``` + +> **Note:** When a PR is squash-merged, the PR title becomes the +> commit message on the main branch. Getting the title right matters. + +### Description + +Every pull request should include a description that helps reviewers +understand the change quickly: + +1. **What and why** — describe the change and its motivation. +2. **Link related issues** — use `Closes #1234` or reference a Taiga + story (e.g. `Taiga #5678`). +3. **Screenshots or recordings** — required for any UI-visible change. +4. **Testing notes** — how did you verify the change? Any edge cases? +5. **Breaking changes** — call out anything that affects existing users + or requires migration steps. + +### Branch naming + +Use a descriptive branch name that reflects the type and scope of the +change: + +``` +/ +``` + +Types: `fix`, `feat`, `refactor`, `docs`, `chore`, `perf`. + +Optionally include the issue number: + +``` +fix/9122-email-blacklisting +feat/export-webp +refactor/layout-sizing +``` + +### Review process + +- Maintainers review PRs when time permits. Please be patient. +- Address review feedback by **pushing new commits** — do not + force-push during review, as it breaks comment threads. +- PRs require at least **one approval** before merge. +- We use **squash-merge** by default. The PR title becomes the final + commit message, so follow the [title format](#title-format) above. + +### What we won't accept + +To save time on both sides, please avoid submitting PRs that: + +- Introduce new dependencies without prior discussion. +- Change the build system or CI configuration without maintainer + approval. +- Mix unrelated changes in a single PR — keep PRs focused on one + concern. +- Skip the [discussion step](#workflow) for non-bug-fix changes. + ### Good first issues We use the `easy fix` label to mark issues appropriate for newcomers. diff --git a/backend/scripts/_env b/backend/scripts/_env index e6ff68b7f4..5367a031b9 100644 --- a/backend/scripts/_env +++ b/backend/scripts/_env @@ -12,7 +12,7 @@ export PENPOT_PUBLIC_URI=https://localhost:3449 export PENPOT_FLAGS="\ $PENPOT_FLAGS \ - enable-login-with-password + enable-login-with-password \ disable-login-with-ldap \ disable-login-with-oidc \ disable-login-with-google \ diff --git a/backend/src/app/rpc/commands/teams_invitations.clj b/backend/src/app/rpc/commands/teams_invitations.clj index 5cffdd0c69..dfc83000a5 100644 --- a/backend/src/app/rpc/commands/teams_invitations.clj +++ b/backend/src/app/rpc/commands/teams_invitations.clj @@ -19,6 +19,7 @@ [app.config :as cf] [app.db :as db] [app.email :as eml] + [app.email.blacklist :as email.blacklist] [app.loggers.audit :as audit] [app.main :as-alias main] [app.rpc :as-alias rpc] @@ -91,6 +92,12 @@ (let [email (profile/clean-email email) member (profile/get-profile-by-email conn email)] + (when (and (email.blacklist/enabled? cfg) + (email.blacklist/contains? cfg email)) + (ex/raise :type :restriction + :code :email-domain-is-not-allowed + :hint "email domain is in the blacklist")) + ;; When we have email verification disabled and invitation user is ;; already present in the database, we proceed to add it to the ;; team as-is, without email roundtrip. diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index daf09e72a7..8fc553ff7e 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -11,6 +11,7 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.email.blacklist :as email.blacklist] [app.http :as http] [app.rpc :as-alias rpc] [app.storage :as sto] @@ -102,6 +103,46 @@ (t/is (= :validation (:type edata))) (t/is (= :member-is-muted (:code edata)))))))) +(t/deftest create-team-invitations-blacklisted-domain + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [profile1 (th/create-profile* 1 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id profile1)}) + data {::th/type :create-team-invitations + ::rpc/profile-id (:id profile1) + :team-id (:id team) + :role :editor}] + + ;; invite from a directly blacklisted domain should fail + (with-redefs [email.blacklist/enabled? (constantly true) + email.blacklist/contains? (fn [_ email] + (clojure.string/ends-with? email "@blacklisted.com"))] + (let [out (th/command! (assoc data :emails ["user@blacklisted.com"]))] + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count @mock))) + (let [edata (-> out :error ex-data)] + (t/is (= :restriction (:type edata))) + (t/is (= :email-domain-is-not-allowed (:code edata)))))) + + ;; invite from a subdomain of a blacklisted domain should also fail + (th/reset-mock! mock) + (with-redefs [email.blacklist/enabled? (constantly true) + email.blacklist/contains? (fn [_ email] + (clojure.string/ends-with? email "@sub.blacklisted.com"))] + (let [out (th/command! (assoc data :emails ["user@sub.blacklisted.com"]))] + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count @mock))) + (let [edata (-> out :error ex-data)] + (t/is (= :restriction (:type edata))) + (t/is (= :email-domain-is-not-allowed (:code edata)))))) + + ;; invite from a non-blacklisted domain should succeed + (th/reset-mock! mock) + (with-redefs [email.blacklist/enabled? (constantly true) + email.blacklist/contains? (constantly false)] + (let [out (th/command! (assoc data :emails ["user@allowed.com"]))] + (t/is (th/success? out)) + (t/is (= 1 (:call-count @mock)))))))) + (t/deftest create-team-invitations-with-request-access (with-mocks [mock {:target 'app.email/send! :return nil}] (let [profile1 (th/create-profile* 1 {:is-active true}) diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index c9c78b9266..b83ff6a79d 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -308,6 +308,9 @@ RUN set -ex; \ less \ jq \ nginx \ + fd-find \ + bat \ + gh \ \ fontconfig \ woff-tools \ diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 96afda2563..993b1623a8 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -195,6 +195,11 @@ (= :email-has-complaints code)) (swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error))) + (and (= :restriction type) + (= :email-domain-is-not-allowed code)) + (st/emit! (ntf/error (tr "errors.email-domain-not-allowed")) + (modal/hide)) + :else (st/emit! (ntf/error (tr "errors.generic")) (modal/hide)))))