-
Notifications
You must be signed in to change notification settings - Fork 528
426 lines (361 loc) · 16.4 KB
/
Copy pathvalidate-pr.yml
File metadata and controls
426 lines (361 loc) · 16.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
name: Validate PR Contribution
# Phase 1 — structural validation.
# Runs on every PR that touches kits/. Posts a single auto-updating comment
# with errors/warnings/info, and manages the following labels:
# - passing-checks → zero errors found
# - requested-improvements → warnings present but no hard errors
# - failing-checks → one or more hard errors found
# - agentkit-challenge → applied to every PR that touches kits/
#
# Structure validated against CONTRIBUTING.md and CLAUDE.md:
#
# kits/<name>/ ← FLAT — no category subdirectory
# lamatic.config.ts ← REQUIRED (all types)
# agent.md ← REQUIRED (all types)
# README.md ← REQUIRED (all types)
# constitutions/default.md ← REQUIRED (all types)
# flows/<flow-name>.ts ← REQUIRED — one .ts file per flow
# .env.example ← REQUIRED (bundles + kits)
# apps/package.json ← REQUIRED (kits only)
# apps/.env.example ← REQUIRED (kits only)
on:
pull_request_target:
types: [opened, edited, synchronize, reopened]
paths:
- 'kits/**'
- '.github/workflows/validate-pr.yml'
jobs:
validate:
if: startsWith(github.event.pull_request.title, 'feat:')
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Checkout PR head
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Fetch base branch
run: git fetch origin ${{ github.event.pull_request.base.ref }}
- name: Validate contribution structure
id: validate
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
run: |
set -euo pipefail
ERRORS=()
WARNINGS=()
NEW_KITS=()
EXISTING_MODIFIED=()
# --- A. Compute diff ---
MERGE_BASE=$(git merge-base "origin/$BASE_REF" HEAD)
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRD "$MERGE_BASE"...HEAD || true)
if [ -z "$CHANGED_FILES" ]; then
SUMMARY_FILE="/tmp/pr_validation_summary.md"
{
echo "## :robot_face: AgentKit Structural Validation"
echo ""
echo "No contribution files detected in this PR."
} > "$SUMMARY_FILE"
cat "$SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
# --- B. Extract unique kit paths (flat — kits/<name>/) ---
declare -A KIT_MAP
OTHER_FILES=()
while IFS= read -r file; do
[ -z "$file" ] && continue
if [[ "$file" == kits/* ]]; then
# Flat structure: kits/<name>/... → 2 levels
kit_path=$(echo "$file" | cut -d/ -f1-2)
KIT_MAP["$kit_path"]=1
else
OTHER_FILES+=("$file")
fi
done <<< "$CHANGED_FILES"
# --- C. Check 1: No edits to existing kits ---
CHECK1_PASS=true
for kit_path in "${!KIT_MAP[@]}"; do
if git ls-tree --name-only "$MERGE_BASE" -- "$kit_path" 2>/dev/null | grep -q .; then
EXISTING_MODIFIED+=("$kit_path")
ERRORS+=("Existing kit modified: $kit_path — feat: PRs should only add new contributions")
CHECK1_PASS=false
else
NEW_KITS+=("$kit_path")
fi
done
# --- D. Check 2: Required root files ---
CHECK2_PASS=true
for kit_path in "${NEW_KITS[@]}"; do
# Required for ALL types
for req in lamatic.config.ts agent.md README.md; do
if [ ! -f "$kit_path/$req" ]; then
ERRORS+=("Missing $req in $kit_path")
CHECK2_PASS=false
fi
done
# Required constitutions/default.md for ALL types
if [ ! -f "$kit_path/constitutions/default.md" ]; then
ERRORS+=("Missing constitutions/default.md in $kit_path")
CHECK2_PASS=false
fi
# flows/ directory must exist
if [ ! -d "$kit_path/flows" ]; then
ERRORS+=("Missing flows/ directory in $kit_path")
CHECK2_PASS=false
fi
done
# --- E. Check 3: Flow files must be .ts files ---
CHECK3_PASS=true
for kit_path in "${NEW_KITS[@]}"; do
if [ ! -d "$kit_path/flows" ]; then
continue
fi
flow_count=0
for flow_file in "$kit_path/flows"/*.ts; do
[ -f "$flow_file" ] || continue
flow_count=$((flow_count + 1))
done
if [ "$flow_count" -eq 0 ]; then
ERRORS+=("No .ts flow files found in $kit_path/flows/ — each flow must be a .ts file exported from Lamatic Studio")
CHECK3_PASS=false
fi
# Warn if old-style flow subdirectories exist (json-based structure)
for flow_dir in "$kit_path/flows"/*/; do
[ -d "$flow_dir" ] || continue
WARNINGS+=("Old-style flow subdirectory found: $flow_dir — flows should be .ts files, not folders. Re-export from Lamatic Studio.")
done
done
# --- F. Check 4: lamatic.config.ts must declare type ---
CHECK4_PASS=true
for kit_path in "${NEW_KITS[@]}"; do
config_file="$kit_path/lamatic.config.ts"
if [ ! -f "$config_file" ]; then
continue
fi
# Check type field is present
if ! grep -qE '"kit"|'\''kit'\''|"bundle"|'\''bundle'\''|"template"|'\''template'\''' "$config_file"; then
ERRORS+=("lamatic.config.ts in $kit_path is missing a valid type field (\"kit\", \"bundle\", or \"template\")")
CHECK4_PASS=false
continue
fi
# Determine type
if grep -qE '"kit"|'\''kit'\''' "$config_file"; then
KIT_TYPE="kit"
elif grep -qE '"bundle"|'\''bundle'\''' "$config_file"; then
KIT_TYPE="bundle"
else
KIT_TYPE="template"
fi
# Kits require apps/package.json and apps/.env.example
if [ "$KIT_TYPE" = "kit" ]; then
if [ ! -f "$kit_path/apps/package.json" ]; then
ERRORS+=("Kit $kit_path is missing apps/package.json — kits must include a Next.js app")
CHECK4_PASS=false
fi
if [ ! -f "$kit_path/apps/.env.example" ]; then
ERRORS+=("Kit $kit_path is missing apps/.env.example")
CHECK4_PASS=false
fi
fi
# Bundles and kits require .env.example at root
if [ "$KIT_TYPE" = "kit" ] || [ "$KIT_TYPE" = "bundle" ]; then
if [ ! -f "$kit_path/.env.example" ]; then
WARNINGS+=("$kit_path is missing .env.example — bundles and kits should include one")
fi
fi
# Check links.github points to kits/<name>
kit_name=$(basename "$kit_path")
if ! grep -q "kits/$kit_name" "$config_file"; then
WARNINGS+=("lamatic.config.ts in $kit_path — links.github should point to kits/$kit_name")
fi
done
# --- G. Check 5: Warn on changes outside kits/ ---
CHECK5_WARN=false
if [ ${#OTHER_FILES[@]} -gt 0 ]; then
CHECK5_WARN=true
for f in "${OTHER_FILES[@]}"; do
WARNINGS+=("File outside kits/ modified: $f")
done
fi
# --- H. Check 6: No committed .env files ---
for kit_path in "${NEW_KITS[@]}"; do
for env_file in "$kit_path/.env" "$kit_path/.env.local" "$kit_path/apps/.env" "$kit_path/apps/.env.local"; do
if [ -f "$env_file" ]; then
ERRORS+=("Committed env file found: $env_file — never commit .env or .env.local, only .env.example")
fi
done
done
# --- I. Build summary ---
SUMMARY_FILE="/tmp/pr_validation_summary.md"
{
echo "## :robot_face: AgentKit Structural Validation"
echo ""
if [ ${#NEW_KITS[@]} -gt 0 ]; then
echo "### New Contributions Detected"
for k in "${NEW_KITS[@]}"; do
kit_name=$(basename "$k")
if [ -f "$k/lamatic.config.ts" ]; then
if grep -q '"kit"' "$k/lamatic.config.ts"; then
ktype="Kit"
elif grep -q '"bundle"' "$k/lamatic.config.ts"; then
ktype="Bundle"
else
ktype="Template"
fi
else
ktype="Unknown"
fi
echo "- **$ktype**: \`$k\`"
done
echo ""
fi
if [ ${#EXISTING_MODIFIED[@]} -gt 0 ]; then
echo "### Existing Kits Modified (not allowed in feat: PRs)"
for k in "${EXISTING_MODIFIED[@]}"; do
echo "- \`$k\`"
done
echo ""
fi
echo "### Check Results"
echo ""
echo "| Check | Status |"
echo "|-------|--------|"
[ "$CHECK1_PASS" = true ] && echo "| No edits to existing kits | :white_check_mark: Pass |" || echo "| No edits to existing kits | :x: Fail |"
[ "$CHECK2_PASS" = true ] && echo "| Required root files present | :white_check_mark: Pass |" || echo "| Required root files present | :x: Fail |"
[ "$CHECK3_PASS" = true ] && echo "| Flow .ts files present | :white_check_mark: Pass |" || echo "| Flow .ts files present | :x: Fail |"
[ "$CHECK4_PASS" = true ] && echo "| lamatic.config.ts valid | :white_check_mark: Pass |" || echo "| lamatic.config.ts valid | :x: Fail |"
[ "$CHECK5_WARN" = false ] && echo "| No changes outside kits/ | :white_check_mark: Pass |" || echo "| No changes outside kits/ | :warning: Warning |"
echo ""
if [ ${#ERRORS[@]} -gt 0 ]; then
echo "### :x: Errors"
echo ""
for err in "${ERRORS[@]}"; do
echo "- $err"
done
echo ""
fi
if [ ${#WARNINGS[@]} -gt 0 ]; then
echo "### :warning: Warnings"
echo ""
for warn in "${WARNINGS[@]}"; do
echo "- $warn"
done
echo ""
fi
if [ ${#ERRORS[@]} -eq 0 ]; then
echo "---"
echo ":tada: All checks passed! This contribution follows the AgentKit structure."
else
echo "---"
echo ":stop_sign: Please fix the errors above before this PR can be merged."
echo ""
echo "Refer to [CONTRIBUTING.md](./CONTRIBUTING.md) and [CLAUDE.md](./CLAUDE.md) for the expected folder structure."
fi
} > "$SUMMARY_FILE"
cat "$SUMMARY_FILE" >> "$GITHUB_STEP_SUMMARY"
if [ ${#ERRORS[@]} -gt 0 ]; then
for err in "${ERRORS[@]}"; do
echo "::error::$err"
done
exit 1
fi
echo "=== ALL CHECKS PASSED ==="
- name: Post validation results as PR comment
if: always()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
SUMMARY_FILE="/tmp/pr_validation_summary.md"
PR_NUMBER="${{ github.event.pull_request.number }}"
REPO="${{ github.repository }}"
if [ ! -f "$SUMMARY_FILE" ]; then
echo "No summary file found, skipping comment."
exit 0
fi
COMMENT_ID=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" \
--jq '.[] | select(.user.type == "Bot" and (.body | startswith("## :robot_face: AgentKit Structural Validation"))) | .id' \
| head -1)
if [ -n "$COMMENT_ID" ]; then
gh api "repos/$REPO/issues/comments/$COMMENT_ID" \
--method PATCH \
-f body="$(cat "$SUMMARY_FILE")"
else
gh api "repos/$REPO/issues/$PR_NUMBER/comments" \
--method POST \
-f body="$(cat "$SUMMARY_FILE")"
fi
- name: Ensure all required labels exist in repo
if: always()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
REPO="${{ github.repository }}"
declare -A LABELS
LABELS["passing-checks"]="0e8a16:All structural validation checks passed"
LABELS["requested-improvements"]="e4a11b:Validation passed with warnings — improvements requested"
LABELS["failing-checks"]="d73a4a:One or more structural validation checks failed"
LABELS["agentkit-challenge"]="1d76db:Submitted as part of the AgentKit Challenge"
LABELS["challenge-failed"]="b60205:Challenge deadline exceeded with unresolved failures"
for LABEL in "${!LABELS[@]}"; do
IFS=':' read -r COLOR DESCRIPTION <<< "${LABELS[$LABEL]}"
if ! gh api "repos/$REPO/labels/$LABEL" --silent 2>/dev/null; then
gh api "repos/$REPO/labels" \
--method POST \
-f name="$LABEL" \
-f color="$COLOR" \
-f description="$DESCRIPTION" 2>/dev/null || true
fi
done
- name: Manage labels and reviewer
if: always()
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER="${{ github.event.pull_request.number }}"
REPO="${{ github.repository }}"
OUTCOME="${{ steps.validate.outcome }}"
WARN_COUNT=$(grep -c "^- " /tmp/pr_validation_summary.md 2>/dev/null | head -1 || echo "0")
# Always apply agentkit-challenge
gh pr edit "$PR_NUMBER" --add-label "agentkit-challenge" --repo "$REPO" || true
if [ "$OUTCOME" = "success" ]; then
if grep -q ":warning: Warnings" /tmp/pr_validation_summary.md 2>/dev/null; then
gh pr edit "$PR_NUMBER" --add-label "requested-improvements" --repo "$REPO" || true
gh pr edit "$PR_NUMBER" --remove-label "passing-checks" --repo "$REPO" 2>/dev/null || true
gh pr edit "$PR_NUMBER" --remove-label "failing-checks" --repo "$REPO" 2>/dev/null || true
else
gh pr edit "$PR_NUMBER" --add-label "passing-checks" --repo "$REPO" || true
gh pr edit "$PR_NUMBER" --remove-label "failing-checks" --repo "$REPO" 2>/dev/null || true
gh pr edit "$PR_NUMBER" --remove-label "requested-improvements" --repo "$REPO" 2>/dev/null || true
fi
else
gh pr edit "$PR_NUMBER" --add-label "failing-checks" --repo "$REPO" || true
gh pr edit "$PR_NUMBER" --remove-label "passing-checks" --repo "$REPO" 2>/dev/null || true
gh pr edit "$PR_NUMBER" --remove-label "requested-improvements" --repo "$REPO" 2>/dev/null || true
# Assign reviewer
gh api "repos/$REPO/pulls/$PR_NUMBER/requested_reviewers" \
--method POST \
-f 'reviewers[]=amanintech' 2>/dev/null || true
# Record failure timestamp for expiry workflow
FAIL_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
EXISTING=$(gh api "repos/$REPO/issues/$PR_NUMBER/comments" \
--jq '.[] | select(.body | contains("agentkit-challenge-failed-at")) | .id' \
| head -1)
if [ -z "$EXISTING" ]; then
printf "%s\n" "<!-- agentkit-challenge-failed-at: $FAIL_TIMESTAMP -->" "" "Failure recorded at $FAIL_TIMESTAMP UTC. If this PR is not fixed within 4 weeks it will be automatically closed." > /tmp/fail_comment.md
gh api "repos/$REPO/issues/$PR_NUMBER/comments" \
--method POST \
-f body="$(cat /tmp/fail_comment.md)" || true
fi
fi
- name: Fail job if validation errored
if: always()
run: |
if [ "${{ steps.validate.outcome }}" != "success" ]; then
echo "::error::Structural validation reported errors. See the PR comment for the full list."
exit 1
fi