🐛 Fix email validation to reject consecutive dots in domain (#10096)

* 🐛 Tighten email validation regex to reject consecutive dots in domain

* 📎 Add minor adjustments to gh-issue-from-pr skill
This commit is contained in:
Andrey Antukh 2026-06-10 19:21:51 +02:00 committed by GitHub
parent 3419f7e60a
commit d1fb1d34a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 70 additions and 12 deletions

View File

@ -39,8 +39,8 @@ Identify:
| Field | Source | Rule |
|-------|--------|------|
| **Title** | PR title | Rewrite from user perspective. Strip leading emoji prefixes (`:bug:`, `:sparkles:`, `:tada:`). Focus on observable behavior. Use imperative mood. |
| **Labels** | PR labels | Copy user-facing labels (`bug`, `enhancement`, `community contribution`). Skip workflow labels (`backport candidate`, `team-qa`). |
| **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. |
@ -101,8 +101,7 @@ Create:
gh issue create \
--repo penpot/penpot \
--title "<Title>" \
--label "<label1>" \
--label "<label2>" \
--label "community contribution" \ # only if PR has this label
--milestone "<milestone>" \
--project "Main" \
--body-file /tmp/issue-body.md
@ -198,12 +197,10 @@ rm -f /tmp/issue-body.md /tmp/pr-body.md
| PR has | Issue gets |
|--------|-----------|
| `bug` | `bug` |
| `enhancement` | `enhancement` |
| `community contribution` | `community contribution` |
| `bug`, `enhancement` | *(skip — redundant with Issue Type)* |
| `backport candidate` | *(skip — workflow label)* |
| `team-qa` | *(skip — workflow label)* |
| No user-facing label | Infer from title: `:bug:``bug`, `:sparkles:``enhancement` |
## Issue Type mapping

View File

@ -448,18 +448,22 @@
::oapi/type "string"
::oapi/format "uuid"}})
(def email-re #"[a-zA-Z0-9_.+-\\\\]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+")
;; Strict email regex aligned with app.common.spec/email-re.
;; Local part: valid RFC chars, no leading/trailing dot, no consecutive dots.
;; Domain: labels can't start/end with hyphen, no empty labels.
;; TLD: at least 2 alphabetic chars.
(def email-re
#"[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,63}")
(defn parse-email
[s]
(if (string? s)
(first (re-seq email-re s))
nil))
(when (and (string? s) (re-matches email-re s))
s))
(defn email-string?
[s]
(and (string? s)
(re-seq email-re s)))
(some? (re-matches email-re s))))
(register!
{:type ::email

View File

@ -188,3 +188,60 @@
(t/is (= false (decode-s "f")))
(t/is (= true (decode-s "1")))
(t/is (= false (decode-s "0")))))
(t/deftest test-email-validation
(t/testing "accepts well-formed email addresses"
(doseq [email ["user@domain.com"
"user.name@domain.com"
"user+tag@domain.com"
"user-name@domain.com"
"user_name@domain.com"
"user123@domain.com"
"USER@DOMAIN.COM"
"u@domain.io"
"user@sub.domain.com"
"user@domain.co.uk"
"user@domain.dev"
"a@bc.co"]]
(t/is (sm/validate ::sm/email email) (str "should accept: " email))
(t/is (= email (sm/decode ::sm/email email sm/json-transformer)))))
(t/testing "rejects domain with consecutive dots (dot-dot)"
(t/is (false? (sm/validate ::sm/email "user@gmail.com..")))
(t/is (false? (sm/validate ::sm/email "user@sub..domain.com")))
(t/is (false? (sm/validate ::sm/email "eissaalbothigi@gmail.com..")))
(t/is (nil? (sm/parse-email "user@gmail.com..")))
(t/is (nil? (sm/parse-email "eissaalbothigi@gmail.com.."))))
(t/testing "rejects domain ending with a dot"
(t/is (false? (sm/validate ::sm/email "user@domain.")))
(t/is (nil? (sm/parse-email "user@domain."))))
(t/testing "rejects domain starting with a dot"
(t/is (false? (sm/validate ::sm/email "user@.domain.com")))
(t/is (nil? (sm/parse-email "user@.domain.com"))))
(t/testing "rejects local part with consecutive dots"
(t/is (false? (sm/validate ::sm/email "user..name@domain.com")))
(t/is (nil? (sm/parse-email "user..name@domain.com"))))
(t/testing "rejects local part starting with a dot"
(t/is (false? (sm/validate ::sm/email ".user@domain.com")))
(t/is (nil? (sm/parse-email ".user@domain.com"))))
(t/testing "rejects label starting or ending with hyphen"
(t/is (false? (sm/validate ::sm/email "user@-domain.com")))
(t/is (false? (sm/validate ::sm/email "user@domain-.com"))))
(t/testing "rejects TLD shorter than 2 chars"
(t/is (false? (sm/validate ::sm/email "user@domain.c"))))
(t/testing "rejects domain without a dot"
(t/is (false? (sm/validate ::sm/email "user@domain"))))
(t/testing "rejects empty or malformed emails"
(t/is (false? (sm/validate ::sm/email "")))
(t/is (false? (sm/validate ::sm/email "@domain.com")))
(t/is (false? (sm/validate ::sm/email "user@")))
(t/is (false? (sm/validate ::sm/email "userdomain.com")))
(t/is (false? (sm/validate ::sm/email "user@@domain.com")))))