Skip to content

Commit e671e7a

Browse files
authored
Merge pull request #5 from tuanductran/copilot/add-rule-template-validation
2 parents 8a04fae + 1c78138 commit e671e7a

6 files changed

Lines changed: 494 additions & 6 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
name: Validate Rules
2+
3+
on:
4+
push:
5+
branches: [main, develop]
6+
pull_request:
7+
branches: [main, develop]
8+
9+
jobs:
10+
validate:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout code
15+
uses: actions/checkout@v4
16+
17+
- name: Setup Node.js
18+
uses: actions/setup-node@v4
19+
with:
20+
node-version: '20'
21+
22+
- name: Setup PNPM
23+
uses: pnpm/action-setup@v4
24+
with:
25+
version: 10.28.1
26+
27+
- name: Install dependencies
28+
run: pnpm install
29+
30+
- name: Run linting
31+
run: pnpm lint
32+
33+
- name: Validate referential integrity
34+
run: |
35+
#!/bin/bash
36+
37+
# Colors for output
38+
RED='\033[0;31m'
39+
GREEN='\033[0;32m'
40+
YELLOW='\033[1;33m'
41+
NC='\033[0m' # No Color
42+
43+
echo "🔍 Checking referential integrity..."
44+
echo ""
45+
46+
# Track if any errors were found
47+
ERRORS_FOUND=0
48+
49+
# Find all SKILL.md files
50+
SKILL_FILES=$(find skills -name "SKILL.md")
51+
52+
for skill_file in $SKILL_FILES; do
53+
skill_dir=$(dirname "$skill_file")
54+
skill_name=$(basename "$skill_dir")
55+
56+
echo "Checking skill: $skill_name"
57+
58+
# Find all rule files in the rules directory
59+
if [ -d "$skill_dir/rules" ]; then
60+
rule_files=$(find "$skill_dir/rules" -name "*.md" -type f)
61+
62+
for rule_file in $rule_files; do
63+
rule_basename=$(basename "$rule_file")
64+
rule_name="${rule_basename%.md}"
65+
66+
# Check if the rule is registered in SKILL.md
67+
if ! grep -q "(rules/$rule_basename)" "$skill_file"; then
68+
echo -e "${RED}❌ ERROR: Rule '$rule_name' exists but is not registered in $skill_file${NC}"
69+
ERRORS_FOUND=1
70+
else
71+
echo -e "${GREEN}✓${NC} $rule_name"
72+
fi
73+
done
74+
fi
75+
76+
echo ""
77+
done
78+
79+
# Check for rules referenced in SKILL.md that don't exist
80+
for skill_file in $SKILL_FILES; do
81+
skill_dir=$(dirname "$skill_file")
82+
skill_name=$(basename "$skill_dir")
83+
84+
# Extract rule references from markdown links like [rule-name](rules/rule-name.md)
85+
# Note: Uses grep -P (Perl regex) which is available on GitHub Actions Ubuntu runners
86+
referenced_rules=$(grep -oP '\[.*?\]\(rules/\K[^)]+(?=\))' "$skill_file" || true)
87+
88+
if [ -n "$referenced_rules" ]; then
89+
echo "Validating referenced rules in $skill_name..."
90+
91+
while IFS= read -r rule_ref; do
92+
rule_path="$skill_dir/rules/$rule_ref"
93+
94+
if [ ! -f "$rule_path" ]; then
95+
echo -e "${RED}❌ ERROR: Rule referenced in $skill_file does not exist: $rule_path${NC}"
96+
ERRORS_FOUND=1
97+
else
98+
echo -e "${GREEN}✓${NC} $(basename $rule_ref)"
99+
fi
100+
done <<< "$referenced_rules"
101+
102+
echo ""
103+
fi
104+
done
105+
106+
# Final result
107+
if [ $ERRORS_FOUND -eq 0 ]; then
108+
echo -e "${GREEN}✅ All referential integrity checks passed!${NC}"
109+
exit 0
110+
else
111+
echo -e "${RED}❌ Referential integrity checks failed. Please fix the errors above.${NC}"
112+
exit 1
113+
fi
114+
115+
- name: Validate YAML frontmatter
116+
run: |
117+
#!/bin/bash
118+
119+
# Colors for output
120+
RED='\033[0;31m'
121+
GREEN='\033[0;32m'
122+
NC='\033[0m'
123+
124+
echo "🔍 Validating YAML frontmatter in rule files..."
125+
echo ""
126+
127+
ERRORS_FOUND=0
128+
129+
# Find all rule files
130+
rule_files=$(find skills/*/rules -name "*.md" -type f)
131+
132+
for rule_file in $rule_files; do
133+
# Check if file has YAML frontmatter
134+
if ! head -n 1 "$rule_file" | grep -q "^---$"; then
135+
echo -e "${RED}❌ ERROR: Missing YAML frontmatter in $rule_file${NC}"
136+
ERRORS_FOUND=1
137+
continue
138+
fi
139+
140+
# Extract frontmatter
141+
frontmatter=$(awk '/^---$/{f=!f;next}f' "$rule_file" | head -n 20)
142+
143+
# Check required fields
144+
required_fields=("title" "impact" "impactDescription" "type" "tags")
145+
146+
for field in "${required_fields[@]}"; do
147+
if ! echo "$frontmatter" | grep -q "^$field:"; then
148+
echo -e "${RED}❌ ERROR: Missing required field '$field' in $rule_file${NC}"
149+
ERRORS_FOUND=1
150+
fi
151+
done
152+
153+
# Validate impact value
154+
impact_value=$(echo "$frontmatter" | grep "^impact:" | cut -d':' -f2 | tr -d ' ')
155+
if [[ ! "$impact_value" =~ ^(HIGH|MEDIUM|LOW)$ ]]; then
156+
echo -e "${RED}❌ ERROR: Invalid impact value '$impact_value' in $rule_file (must be HIGH, MEDIUM, or LOW)${NC}"
157+
ERRORS_FOUND=1
158+
fi
159+
160+
# Validate type value
161+
type_value=$(echo "$frontmatter" | grep "^type:" | cut -d':' -f2 | tr -d ' ')
162+
if [[ ! "$type_value" =~ ^(capability|efficiency)$ ]]; then
163+
echo -e "${RED}❌ ERROR: Invalid type value '$type_value' in $rule_file (must be capability or efficiency)${NC}"
164+
ERRORS_FOUND=1
165+
fi
166+
167+
# Check for Impact Line after H1 (skip all blank lines)
168+
line_after_h1=$(awk '/^# /{found=1; next} found && NF {print; exit}' "$rule_file")
169+
if ! echo "$line_after_h1" | grep -q '^\*\*Impact:'; then
170+
echo -e "${RED}❌ ERROR: Missing Impact Line after H1 title in $rule_file${NC}"
171+
ERRORS_FOUND=1
172+
fi
173+
done
174+
175+
if [ $ERRORS_FOUND -eq 0 ]; then
176+
echo -e "${GREEN}✅ All YAML frontmatter validations passed!${NC}"
177+
exit 0
178+
else
179+
echo -e "${RED}❌ YAML frontmatter validation failed. Please fix the errors above.${NC}"
180+
exit 1
181+
fi

CLAUDE.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ The repository is organized by skill directories inside the `skills/` folder:
2929
- `skills/nextdns-api` - Rules for API integration (Auth, Analytics, Logs, Profiles).
3030
- `skills/nextdns-cli` - Rules for CLI installation, daemon control, and advanced routing.
3131
- `skills/nextdns-ui` - Rules for Web Dashboard settings based on best practices and threat modeling.
32+
- `skills/integrations` - Rules for third-party platform integrations (Tailscale, Home Assistant, Ubiquiti, etc.).
3233

3334
Each skill directory follows this internal structure:
3435
- `SKILL.md` - Entry point containing skill metadata and a table mapping rules to keywords.
@@ -71,18 +72,23 @@ Immediately following the H1 heading, a bolded summary must exist:
7172
- `skills/nextdns-api/SKILL.md` - Schema and mapping for API-related tasks.
7273
- `skills/nextdns-cli/SKILL.md` - Schema and mapping for CLI/Terminal tasks.
7374
- `skills/nextdns-ui/SKILL.md` - Schema and mapping for Web UI/Configuration tasks.
75+
- `skills/integrations/SKILL.md` - Schema and mapping for third-party integration tasks.
76+
- `templates/rule-template.md` - Standardized template for creating new rules.
77+
- `data/schemas/profile.json` - Mock NextDNS Profile API response for testing.
7478
- `package.json` - Defines linting scripts and project metadata.
7579
- `.markdownlint.yml` - Global markdown styling rules.
80+
- `.github/workflows/validate-rules.yml` - CI/CD workflow for automated validation.
7681

7782
## Making Changes
7883

7984
### Adding a New Rule
8085

81-
1. Create a new markdown file in the relevant `skills/<name>/rules/` folder using `kebab-case.md`.
82-
2. Add the required YAML frontmatter at the top.
83-
3. Include the H1 title followed by the bolded Impact Line.
84-
4. Add the entry to the Capability or Efficiency table in the parent `SKILL.md`.
85-
5. Run `pnpm lint:fix` to ensure compliance.
86+
1. Use the template from `templates/rule-template.md` as a starting point.
87+
2. Create a new markdown file in the relevant `skills/<name>/rules/` folder using `kebab-case.md`.
88+
3. Add the required YAML frontmatter at the top.
89+
4. Include the H1 title followed by the bolded Impact Line.
90+
5. Add the entry to the Capability or Efficiency table in the parent `SKILL.md`.
91+
6. Run `pnpm lint:fix` to ensure compliance.
8692

8793
### Updating Technical Knowledge
8894

data/schemas/profile.json

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
{
2+
"id": "abc123",
3+
"fingerprint": "abc12",
4+
"name": "Home Network",
5+
"security": {
6+
"threatIntelligenceFeeds": true,
7+
"aiThreatDetection": true,
8+
"googleSafeBrowsing": false,
9+
"cryptojacking": true,
10+
"dnsRebinding": true,
11+
"idnHomographs": true,
12+
"typosquatting": true,
13+
"dga": true,
14+
"nrd": true,
15+
"ddns": false,
16+
"parking": true,
17+
"csam": true,
18+
"tlds": [
19+
"zip",
20+
"mov",
21+
"country"
22+
]
23+
},
24+
"privacy": {
25+
"blocklists": [
26+
{
27+
"id": "hagezi-multi-pro",
28+
"name": "HaGeZi - Multi PRO"
29+
},
30+
{
31+
"id": "oisd-full",
32+
"name": "OISD - Full"
33+
},
34+
{
35+
"id": "hostsvn",
36+
"name": "hostsVN"
37+
}
38+
],
39+
"natives": [
40+
"apple",
41+
"samsung",
42+
"xiaomi",
43+
"windows",
44+
"alexa",
45+
"roku",
46+
"sonos",
47+
"huawei"
48+
],
49+
"disguisedTrackers": true,
50+
"allowAffiliate": true
51+
},
52+
"parentalControl": {
53+
"services": [
54+
"tiktok",
55+
"facebook",
56+
"instagram",
57+
"twitter",
58+
"youtube"
59+
],
60+
"categories": [
61+
"gambling",
62+
"dating",
63+
"piracy"
64+
],
65+
"safeSearch": true,
66+
"youtubeRestrictedMode": true,
67+
"blockBypass": true,
68+
"recreation": {
69+
"enabled": true,
70+
"monday": {
71+
"start": "18:00",
72+
"end": "21:00"
73+
},
74+
"tuesday": {
75+
"start": "18:00",
76+
"end": "21:00"
77+
},
78+
"wednesday": {
79+
"start": "18:00",
80+
"end": "21:00"
81+
},
82+
"thursday": {
83+
"start": "18:00",
84+
"end": "21:00"
85+
},
86+
"friday": {
87+
"start": "18:00",
88+
"end": "23:00"
89+
},
90+
"saturday": {
91+
"start": "10:00",
92+
"end": "23:00"
93+
},
94+
"sunday": {
95+
"start": "10:00",
96+
"end": "21:00"
97+
}
98+
}
99+
},
100+
"denylist": [
101+
"ads.example.com",
102+
"tracker.example.net",
103+
"malicious.example.org"
104+
],
105+
"allowlist": [
106+
"important-service.com",
107+
"business-app.example.com",
108+
"email-tracking.service.net"
109+
],
110+
"settings": {
111+
"logs": {
112+
"enabled": true,
113+
"location": "ch",
114+
"retention": 2592000
115+
},
116+
"blockPage": {
117+
"enabled": true
118+
},
119+
"performance": {
120+
"ecs": true,
121+
"cacheBoost": true,
122+
"cnameFlattening": true
123+
},
124+
"rewrites": [
125+
{
126+
"name": "local-server.home",
127+
"answer": "192.168.1.100"
128+
},
129+
{
130+
"name": "nas.home",
131+
"answer": "192.168.1.50"
132+
}
133+
]
134+
},
135+
"analytics": {
136+
"enabled": true,
137+
"retention": 90
138+
},
139+
"setup": {
140+
"linkedIp": {
141+
"enabled": true,
142+
"autodetect": true,
143+
"ddnsHostname": "my-router.ddns.net",
144+
"updateToken": "abc123xyz789"
145+
},
146+
"devices": [
147+
{
148+
"id": "device1",
149+
"name": "John's iPhone",
150+
"model": "iPhone 14 Pro",
151+
"icon": "iphone",
152+
"profile": "abc123"
153+
},
154+
{
155+
"id": "device2",
156+
"name": "Smart TV",
157+
"model": "Samsung 2023",
158+
"icon": "tv",
159+
"profile": "abc123"
160+
},
161+
{
162+
"id": "device3",
163+
"name": "Home Router",
164+
"model": "OpenWrt",
165+
"icon": "router",
166+
"profile": "abc123"
167+
}
168+
]
169+
}
170+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"packageManager": "pnpm@10.28.1",
1616
"engines": {
17-
"node": ">=18.0.0"
17+
"node": ">=20.0.0"
1818
},
1919
"scripts": {
2020
"lint": "markdownlint --ignore-path=.gitignore . && case-police 'skills/**/*.md'",

0 commit comments

Comments
 (0)