@codyswann/lisa 2.195.8 → 2.196.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/all/github-rulesets/base.json +64 -0
  2. package/all/github-rulesets/prevent-delete.json +25 -0
  3. package/all/github-rulesets/protect-tags.json +31 -0
  4. package/cdk/github-rulesets/cdk-validation.json +43 -0
  5. package/expo/github-rulesets/playwright.json +38 -0
  6. package/package.json +2 -2
  7. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  9. package/plugins/lisa/.codex-plugin/skills/lisa-setup-github-repo/SKILL.md +66 -0
  10. package/plugins/lisa/.codex-plugin/skills/lisa-setup-github-repo/agents/openai.yaml +4 -0
  11. package/plugins/lisa/commands/lisa/setup/github-repo.md +7 -0
  12. package/plugins/lisa/skills/lisa-setup-github-repo/SKILL.md +66 -0
  13. package/plugins/lisa/skills/lisa-setup-github-repo/agents/openai.yaml +4 -0
  14. package/plugins/lisa-agy/commands/lisa/setup/github-repo.md +7 -0
  15. package/plugins/lisa-agy/plugin.json +1 -1
  16. package/plugins/lisa-agy/skills/lisa-setup-github-repo/SKILL.md +66 -0
  17. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/commands/lisa/setup/github-repo.md +7 -0
  24. package/plugins/lisa-copilot/skills/lisa-setup-github-repo/SKILL.md +66 -0
  25. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-cursor/commands/lisa/setup/github-repo.md +7 -0
  27. package/plugins/lisa-cursor/skills/lisa-setup-github-repo/SKILL.md +66 -0
  28. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  30. package/plugins/lisa-expo-agy/plugin.json +1 -1
  31. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  51. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-rails-agy/plugin.json +1 -1
  56. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  61. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  66. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/src/base/commands/lisa/setup/github-repo.md +7 -0
  69. package/plugins/src/base/skills/lisa-setup-github-repo/SKILL.md +66 -0
  70. package/rails/github-rulesets/quality-checks.json +51 -0
  71. package/scripts/cleanup-github-branches.sh +96 -72
  72. package/scripts/cleanup-local-merged-branches.sh +110 -0
  73. package/scripts/cleanup-worktrees.sh +222 -0
  74. package/scripts/lisa-github-repo-settings.sh +152 -0
  75. package/scripts/lisa-github-repo-setup.sh +63 -0
  76. package/scripts/lisa-github-rulesets.sh +549 -0
  77. package/scripts/setup-deploy-key.sh +61 -2
  78. package/typescript/github-rulesets/quality-checks.json +67 -0
  79. package/typescript/github-rulesets/base.json +0 -33
@@ -0,0 +1,549 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # lisa-github-rulesets.sh
4
+ #
5
+ # Applies GitHub repository rulesets from Lisa's project type directories.
6
+ # Reads ruleset templates from github-rulesets/ folders and uses the gh CLI
7
+ # to create or update them on the target repository.
8
+ #
9
+ # Usage:
10
+ # lisa-github-rulesets.sh [options] [project-path]
11
+ #
12
+ # Options:
13
+ # -n, --dry-run Show what would be done without making changes
14
+ # -y, --yes Non-interactive mode (skip confirmations)
15
+ # -v, --verbose Show detailed output
16
+ # -h, --help Show this help message
17
+ #
18
+ # Requires:
19
+ # - gh CLI (authenticated with repo admin permissions)
20
+ # - jq
21
+ #
22
+
23
+ set -eo pipefail
24
+
25
+ # Colors
26
+ RED='\033[0;31m'
27
+ GREEN='\033[0;32m'
28
+ YELLOW='\033[0;33m'
29
+ BLUE='\033[0;34m'
30
+ NC='\033[0m' # No Color
31
+
32
+ # Script directory (this file lives in scripts/; templates live one level up)
33
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
34
+ LISA_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
35
+
36
+ # Default options
37
+ DRY_RUN=false
38
+ YES_MODE=false
39
+ VERBOSE=false
40
+ PROJECT_PATH=""
41
+
42
+ # Project type hierarchy (child -> parent)
43
+ # Using a function to avoid associative array issues with set -u
44
+ get_parent_type() {
45
+ local type="$1"
46
+ case "$type" in
47
+ typescript) echo "" ;;
48
+ npm-package) echo "typescript" ;;
49
+ expo) echo "typescript" ;;
50
+ nestjs) echo "typescript" ;;
51
+ cdk) echo "typescript" ;;
52
+ rails) echo "" ;;
53
+ *) echo "" ;;
54
+ esac
55
+ }
56
+
57
+ ##############################################################################
58
+ # Utility Functions
59
+ ##############################################################################
60
+
61
+ log_info() {
62
+ echo -e "${BLUE}ℹ${NC} $1"
63
+ }
64
+
65
+ log_success() {
66
+ echo -e "${GREEN}✓${NC} $1"
67
+ }
68
+
69
+ log_warning() {
70
+ echo -e "${YELLOW}⚠${NC} $1"
71
+ }
72
+
73
+ log_error() {
74
+ echo -e "${RED}✗${NC} $1" >&2
75
+ }
76
+
77
+ log_verbose() {
78
+ if [[ "$VERBOSE" == "true" ]]; then
79
+ echo -e " $1"
80
+ fi
81
+ }
82
+
83
+ show_help() {
84
+ cat << 'EOF'
85
+ lisa-github-rulesets.sh - Apply GitHub repository rulesets from Lisa templates
86
+
87
+ USAGE:
88
+ lisa-github-rulesets.sh [OPTIONS] [PROJECT_PATH]
89
+
90
+ ARGUMENTS:
91
+ PROJECT_PATH Path to the project (defaults to current directory)
92
+
93
+ OPTIONS:
94
+ -n, --dry-run Show what would be done without making API calls
95
+ -y, --yes Non-interactive mode (skip confirmations)
96
+ -v, --verbose Show detailed output
97
+ -h, --help Show this help message
98
+
99
+ DESCRIPTION:
100
+ Reads ruleset templates from Lisa's project type directories
101
+ (all/github-rulesets/, typescript/github-rulesets/, etc.) and
102
+ applies them to the target repository using the GitHub API.
103
+
104
+ Templates are applied in order: all -> parent types -> specific type.
105
+ If a ruleset with the same name exists, it will be updated.
106
+
107
+ REQUIREMENTS:
108
+ - gh CLI must be installed and authenticated
109
+ - User must have admin permissions on the repository
110
+ - jq must be installed for JSON processing
111
+
112
+ EXAMPLES:
113
+ # Apply rulesets to current directory's repo
114
+ lisa-github-rulesets.sh
115
+
116
+ # Dry run to see what would happen
117
+ lisa-github-rulesets.sh --dry-run /path/to/project
118
+
119
+ # Non-interactive mode
120
+ lisa-github-rulesets.sh --yes /path/to/project
121
+ EOF
122
+ }
123
+
124
+ ##############################################################################
125
+ # Prerequisite Checks
126
+ ##############################################################################
127
+
128
+ check_prerequisites() {
129
+ local missing=()
130
+
131
+ if ! command -v gh &> /dev/null; then
132
+ missing+=("gh (GitHub CLI)")
133
+ fi
134
+
135
+ if ! command -v jq &> /dev/null; then
136
+ missing+=("jq")
137
+ fi
138
+
139
+ if [[ ${#missing[@]} -gt 0 ]]; then
140
+ log_error "Missing required tools: ${missing[*]}"
141
+ exit 1
142
+ fi
143
+
144
+ # Check gh authentication
145
+ if ! gh auth status &> /dev/null; then
146
+ log_error "GitHub CLI is not authenticated. Run 'gh auth login' first."
147
+ exit 1
148
+ fi
149
+ }
150
+
151
+ ##############################################################################
152
+ # Project Detection
153
+ ##############################################################################
154
+
155
+ detect_project_types() {
156
+ local project_path="$1"
157
+ local detected_types=()
158
+
159
+ # TypeScript detection
160
+ if [[ -f "$project_path/tsconfig.json" ]]; then
161
+ detected_types+=("typescript")
162
+ elif [[ -f "$project_path/package.json" ]]; then
163
+ if jq -e '.devDependencies.typescript // .dependencies.typescript' "$project_path/package.json" &> /dev/null; then
164
+ detected_types+=("typescript")
165
+ fi
166
+ fi
167
+
168
+ # npm-package detection (not private and has main/bin/exports/files)
169
+ if [[ -f "$project_path/package.json" ]]; then
170
+ local is_private
171
+ is_private=$(jq -r '.private // false' "$project_path/package.json")
172
+ if [[ "$is_private" != "true" ]]; then
173
+ if jq -e '.main // .bin // .exports // .files' "$project_path/package.json" &> /dev/null; then
174
+ detected_types+=("npm-package")
175
+ fi
176
+ fi
177
+ fi
178
+
179
+ # Expo detection
180
+ if [[ -f "$project_path/app.json" ]] || [[ -f "$project_path/eas.json" ]]; then
181
+ detected_types+=("expo")
182
+ elif [[ -f "$project_path/package.json" ]]; then
183
+ if jq -e '.dependencies.expo // .devDependencies.expo' "$project_path/package.json" &> /dev/null; then
184
+ detected_types+=("expo")
185
+ fi
186
+ fi
187
+
188
+ # NestJS detection
189
+ if [[ -f "$project_path/nest-cli.json" ]]; then
190
+ detected_types+=("nestjs")
191
+ elif [[ -f "$project_path/package.json" ]]; then
192
+ if jq -e '.dependencies["@nestjs/core"] // .devDependencies["@nestjs/core"]' "$project_path/package.json" &> /dev/null; then
193
+ detected_types+=("nestjs")
194
+ fi
195
+ fi
196
+
197
+ # CDK detection
198
+ if [[ -f "$project_path/cdk.json" ]]; then
199
+ detected_types+=("cdk")
200
+ elif [[ -f "$project_path/package.json" ]]; then
201
+ if jq -e '.dependencies["aws-cdk-lib"] // .devDependencies["aws-cdk-lib"]' "$project_path/package.json" &> /dev/null; then
202
+ detected_types+=("cdk")
203
+ fi
204
+ fi
205
+
206
+ # Rails detection
207
+ if [[ -f "$project_path/Gemfile" ]] && grep -q "rails" "$project_path/Gemfile" 2>/dev/null; then
208
+ detected_types+=("rails")
209
+ fi
210
+
211
+ echo "${detected_types[@]}"
212
+ }
213
+
214
+ expand_types_with_parents() {
215
+ local -a input_types=("$@")
216
+ local -a expanded=()
217
+ local seen=""
218
+
219
+ for type in "${input_types[@]}"; do
220
+ local current="$type"
221
+ local -a chain=()
222
+
223
+ # Build chain from type to root
224
+ while [[ -n "$current" ]]; do
225
+ chain+=("$current")
226
+ current=$(get_parent_type "$current")
227
+ done
228
+
229
+ # Add in reverse order (parent first)
230
+ for ((i=${#chain[@]}-1; i>=0; i--)); do
231
+ local t="${chain[$i]}"
232
+ if [[ ! " $seen " =~ " $t " ]]; then
233
+ expanded+=("$t")
234
+ seen="$seen $t"
235
+ fi
236
+ done
237
+ done
238
+
239
+ echo "${expanded[@]}"
240
+ }
241
+
242
+ ##############################################################################
243
+ # Repository Info
244
+ ##############################################################################
245
+
246
+ get_repo_info() {
247
+ local project_path="$1"
248
+
249
+ cd "$project_path"
250
+
251
+ if ! git rev-parse --git-dir &> /dev/null; then
252
+ log_error "Not a git repository: $project_path"
253
+ exit 1
254
+ fi
255
+
256
+ local repo_info
257
+ repo_info=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null) || {
258
+ log_error "Could not determine repository. Make sure you're in a git repo with a GitHub remote."
259
+ exit 1
260
+ }
261
+
262
+ echo "$repo_info"
263
+ }
264
+
265
+ ##############################################################################
266
+ # Ruleset Operations
267
+ ##############################################################################
268
+
269
+ find_ruleset_by_name() {
270
+ local rulesets_json="$1"
271
+ local name="$2"
272
+ echo "$rulesets_json" | jq -r --arg name "$name" '.[] | select(.name == $name) | .id // empty'
273
+ }
274
+
275
+ strip_readonly_fields() {
276
+ local json="$1"
277
+ # Remove fields that are read-only or repository-specific
278
+ echo "$json" | jq 'del(.id, .source_type, .source, .node_id, .created_at, .updated_at, ._links, .current_user_can_bypass)'
279
+ }
280
+
281
+ # GitHub Actions integration id — its checks only ever report when the project
282
+ # actually has workflows, so requiring them on a workflow-less repo (e.g. a
283
+ # wiki) would block every PR forever.
284
+ ACTIONS_INTEGRATION_ID=15368
285
+
286
+ strip_actions_checks_if_no_workflows() {
287
+ local json="$1"
288
+ local project_path="$2"
289
+
290
+ if [[ -d "$project_path/.github/workflows" ]]; then
291
+ echo "$json"
292
+ return 0
293
+ fi
294
+
295
+ echo "$json" | jq --argjson actions "$ACTIONS_INTEGRATION_ID" '
296
+ if .rules then
297
+ .rules |= (
298
+ map(
299
+ if .type == "required_status_checks" then
300
+ .parameters.required_status_checks |=
301
+ map(select(.integration_id != $actions))
302
+ else . end
303
+ )
304
+ | map(select(
305
+ .type != "required_status_checks"
306
+ or (.parameters.required_status_checks | length) > 0
307
+ ))
308
+ )
309
+ else . end'
310
+ }
311
+
312
+ apply_ruleset() {
313
+ local repo="$1"
314
+ local template_file="$2"
315
+ local existing_rulesets="$3"
316
+ local project_path="$4"
317
+
318
+ local template_content
319
+ template_content=$(cat "$template_file")
320
+
321
+ local ruleset_name
322
+ ruleset_name=$(echo "$template_content" | jq -r '.name')
323
+
324
+ local clean_template
325
+ clean_template=$(strip_readonly_fields "$template_content")
326
+ clean_template=$(strip_actions_checks_if_no_workflows "$clean_template" "$project_path")
327
+
328
+ # A template whose rules were entirely stripped has nothing to enforce here.
329
+ if [[ $(echo "$clean_template" | jq '(.rules // [1]) | length') -eq 0 ]]; then
330
+ log_warning "Skipping ruleset '$ruleset_name' — no applicable rules for this project (no workflows)"
331
+ return 0
332
+ fi
333
+
334
+ local existing_id
335
+ existing_id=$(find_ruleset_by_name "$existing_rulesets" "$ruleset_name")
336
+
337
+ if [[ "$DRY_RUN" == "true" ]]; then
338
+ if [[ -n "$existing_id" ]]; then
339
+ log_info "[DRY RUN] Would update ruleset '$ruleset_name' (id: $existing_id)"
340
+ else
341
+ log_info "[DRY RUN] Would create ruleset '$ruleset_name'"
342
+ fi
343
+ log_verbose "Template: $template_file"
344
+ return 0
345
+ fi
346
+
347
+ # Create temp file for the request body
348
+ local temp_file
349
+ temp_file=$(mktemp)
350
+ echo "$clean_template" > "$temp_file"
351
+
352
+ if [[ -n "$existing_id" ]]; then
353
+ log_info "Updating ruleset '$ruleset_name' (id: $existing_id)..."
354
+ if gh api -X PUT "repos/$repo/rulesets/$existing_id" --input "$temp_file" > /dev/null; then
355
+ log_success "Updated ruleset '$ruleset_name'"
356
+ else
357
+ log_error "Failed to update ruleset '$ruleset_name'"
358
+ rm -f "$temp_file"
359
+ return 1
360
+ fi
361
+ else
362
+ log_info "Creating ruleset '$ruleset_name'..."
363
+ if gh api -X POST "repos/$repo/rulesets" --input "$temp_file" > /dev/null; then
364
+ log_success "Created ruleset '$ruleset_name'"
365
+ else
366
+ log_error "Failed to create ruleset '$ruleset_name'"
367
+ rm -f "$temp_file"
368
+ return 1
369
+ fi
370
+ fi
371
+
372
+ rm -f "$temp_file"
373
+ }
374
+
375
+ ##############################################################################
376
+ # Main Logic
377
+ ##############################################################################
378
+
379
+ collect_templates() {
380
+ local -a types=("$@")
381
+ local -a templates=()
382
+
383
+ # Always include 'all' first
384
+ local all_dir="$LISA_ROOT/all/github-rulesets"
385
+ if [[ -d "$all_dir" ]]; then
386
+ for file in "$all_dir"/*.json; do
387
+ [[ -f "$file" ]] && templates+=("$file")
388
+ done
389
+ fi
390
+
391
+ # Then add type-specific templates in order
392
+ for type in "${types[@]}"; do
393
+ local type_dir="$LISA_ROOT/$type/github-rulesets"
394
+ if [[ -d "$type_dir" ]]; then
395
+ for file in "$type_dir"/*.json; do
396
+ [[ -f "$file" ]] && templates+=("$file")
397
+ done
398
+ fi
399
+ done
400
+
401
+ echo "${templates[@]}"
402
+ }
403
+
404
+ main() {
405
+ # Parse arguments
406
+ while [[ $# -gt 0 ]]; do
407
+ case $1 in
408
+ -n|--dry-run)
409
+ DRY_RUN=true
410
+ shift
411
+ ;;
412
+ -y|--yes)
413
+ YES_MODE=true
414
+ shift
415
+ ;;
416
+ -v|--verbose)
417
+ VERBOSE=true
418
+ shift
419
+ ;;
420
+ -h|--help)
421
+ show_help
422
+ exit 0
423
+ ;;
424
+ -*)
425
+ log_error "Unknown option: $1"
426
+ show_help
427
+ exit 1
428
+ ;;
429
+ *)
430
+ PROJECT_PATH="$1"
431
+ shift
432
+ ;;
433
+ esac
434
+ done
435
+
436
+ # Default to current directory
437
+ PROJECT_PATH="${PROJECT_PATH:-.}"
438
+ PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
439
+
440
+ log_info "Lisa GitHub Rulesets"
441
+ echo ""
442
+
443
+ # Check prerequisites
444
+ check_prerequisites
445
+
446
+ # Get repository info
447
+ local repo
448
+ repo=$(get_repo_info "$PROJECT_PATH")
449
+ log_info "Repository: $repo"
450
+
451
+ # Detect project types
452
+ local detected_types_str
453
+ detected_types_str=$(detect_project_types "$PROJECT_PATH")
454
+
455
+ if [[ -z "$detected_types_str" ]]; then
456
+ log_warning "No specific project types detected, using 'all' templates only"
457
+ detected_types_str=""
458
+ fi
459
+
460
+ # Convert to array safely
461
+ local -a detected_types=()
462
+ if [[ -n "$detected_types_str" ]]; then
463
+ read -ra detected_types <<< "$detected_types_str"
464
+ fi
465
+
466
+ # Expand with parent types
467
+ local -a expanded_types=()
468
+ if [[ ${#detected_types[@]} -gt 0 ]]; then
469
+ local expanded_str
470
+ expanded_str=$(expand_types_with_parents "${detected_types[@]}")
471
+ read -ra expanded_types <<< "$expanded_str"
472
+ fi
473
+
474
+ if [[ ${#expanded_types[@]} -gt 0 ]]; then
475
+ log_info "Detected types: ${expanded_types[*]}"
476
+ fi
477
+
478
+ # Collect templates
479
+ local templates_str
480
+ templates_str=$(collect_templates "${expanded_types[@]}")
481
+
482
+ local -a templates=()
483
+ if [[ -n "$templates_str" ]]; then
484
+ read -ra templates <<< "$templates_str"
485
+ fi
486
+
487
+ if [[ ${#templates[@]} -eq 0 ]]; then
488
+ log_warning "No ruleset templates found"
489
+ exit 0
490
+ fi
491
+
492
+ log_info "Found ${#templates[@]} ruleset template(s)"
493
+ for t in "${templates[@]}"; do
494
+ log_verbose " - $t"
495
+ done
496
+ echo ""
497
+
498
+ # Confirmation prompt
499
+ if [[ "$DRY_RUN" == "false" ]] && [[ "$YES_MODE" == "false" ]]; then
500
+ echo -n "Apply rulesets to $repo? [y/N] "
501
+ read -r response
502
+ if [[ ! "$response" =~ ^[Yy]$ ]]; then
503
+ log_info "Aborted"
504
+ exit 0
505
+ fi
506
+ echo ""
507
+ fi
508
+
509
+ # Get existing rulesets — a 403 here means the plan doesn't support rulesets
510
+ # (private repo on a free personal plan); that's a skip, not a failure.
511
+ log_info "Fetching existing rulesets..."
512
+ local existing_rulesets
513
+ if ! existing_rulesets=$(gh api "repos/$repo/rulesets" 2>&1); then
514
+ if echo "$existing_rulesets" | grep -qi "upgrade to github\|HTTP 403"; then
515
+ log_warning "Rulesets are not available on this repository's plan — skipping"
516
+ exit 0
517
+ fi
518
+ log_error "Could not fetch rulesets: $existing_rulesets"
519
+ exit 1
520
+ fi
521
+ local existing_count
522
+ existing_count=$(echo "$existing_rulesets" | jq 'length')
523
+ log_verbose "Found $existing_count existing ruleset(s)"
524
+
525
+ # Apply each template
526
+ local success_count=0
527
+ local fail_count=0
528
+
529
+ for template in "${templates[@]}"; do
530
+ if apply_ruleset "$repo" "$template" "$existing_rulesets" "$PROJECT_PATH"; then
531
+ success_count=$((success_count + 1))
532
+ else
533
+ fail_count=$((fail_count + 1))
534
+ fi
535
+ done
536
+
537
+ echo ""
538
+ if [[ "$DRY_RUN" == "true" ]]; then
539
+ log_info "Dry run complete. ${#templates[@]} ruleset(s) would be applied."
540
+ else
541
+ log_success "Applied $success_count ruleset(s)"
542
+ if [[ $fail_count -gt 0 ]]; then
543
+ log_warning "$fail_count ruleset(s) failed"
544
+ exit 1
545
+ fi
546
+ fi
547
+ }
548
+
549
+ main "$@"
@@ -6,15 +6,27 @@
6
6
  # with write access, enabling GitHub Actions to push to protected branches.
7
7
  #
8
8
  # Usage:
9
- # ./scripts/setup-deploy-key.sh
9
+ # ./scripts/setup-deploy-key.sh [--yes]
10
+ #
11
+ # Options:
12
+ # -y, --yes Non-interactive mode: skips if a deploy key + DEPLOY_KEY
13
+ # secret already exist, otherwise creates both without
14
+ # prompting and never leaves key files on disk.
10
15
  #
11
16
  # Requirements:
12
17
  # - ssh-keygen (usually pre-installed)
13
- # - gh CLI (optional, for automatic setup)
18
+ # - gh CLI (optional, for automatic setup; required for --yes)
14
19
  #
15
20
 
16
21
  set -euo pipefail
17
22
 
23
+ YES_MODE=false
24
+ for arg in "$@"; do
25
+ case "$arg" in
26
+ -y|--yes) YES_MODE=true ;;
27
+ esac
28
+ done
29
+
18
30
  # Colors for output
19
31
  RED='\033[0;31m'
20
32
  GREEN='\033[0;32m'
@@ -68,6 +80,53 @@ echo ""
68
80
  echo "Repository: $OWNER/$REPO"
69
81
  echo ""
70
82
 
83
+ # Non-interactive mode: idempotent — skip when already configured, and keep
84
+ # generated key material in a private temp dir that is always cleaned up.
85
+ KEY_TITLE="GitHub Actions Deploy Key"
86
+ if [[ "$YES_MODE" == "true" ]]; then
87
+ if ! command -v gh &>/dev/null || ! gh auth status &>/dev/null; then
88
+ print_error "--yes mode requires an authenticated gh CLI."
89
+ exit 1
90
+ fi
91
+
92
+ HAS_KEY=$(gh repo deploy-key list --repo "$OWNER/$REPO" --json title \
93
+ --jq --arg t "$KEY_TITLE" '[.[] | select(.title == $t)] | length' 2>/dev/null || echo 0)
94
+ HAS_SECRET=$(gh secret list --repo "$OWNER/$REPO" --json name \
95
+ --jq '[.[] | select(.name == "DEPLOY_KEY")] | length' 2>/dev/null || echo 0)
96
+ if [[ "$HAS_KEY" -gt 0 && "$HAS_SECRET" -gt 0 ]]; then
97
+ print_success "Deploy key and DEPLOY_KEY secret already configured — nothing to do."
98
+ exit 0
99
+ fi
100
+
101
+ TEMP_KEY_DIR=$(mktemp -d)
102
+ trap 'rm -rf "$TEMP_KEY_DIR"' EXIT
103
+ KEY_FILE="$TEMP_KEY_DIR/deploy_key"
104
+
105
+ print_step "Generating SSH key pair..."
106
+ ssh-keygen -t ed25519 -C "github-actions-deploy-key-${REPO}" -f "$KEY_FILE" -N "" -q
107
+
108
+ # A stale key under the same title blocks re-adding; replace it.
109
+ if [[ "$HAS_KEY" -gt 0 ]]; then
110
+ gh repo deploy-key list --repo "$OWNER/$REPO" --json id,title \
111
+ --jq --arg t "$KEY_TITLE" '.[] | select(.title == $t) | .id' 2>/dev/null \
112
+ | while read -r key_id; do
113
+ gh repo deploy-key delete "$key_id" --repo "$OWNER/$REPO" || true
114
+ done
115
+ fi
116
+
117
+ print_step "Adding deploy key to repository..."
118
+ gh repo deploy-key add "${KEY_FILE}.pub" --repo "$OWNER/$REPO" \
119
+ --title "$KEY_TITLE" --allow-write
120
+ print_success "Deploy key added with write access"
121
+
122
+ print_step "Adding DEPLOY_KEY secret..."
123
+ gh secret set DEPLOY_KEY --repo "$OWNER/$REPO" < "$KEY_FILE"
124
+ print_success "DEPLOY_KEY secret added"
125
+
126
+ print_success "Deploy key setup complete!"
127
+ exit 0
128
+ fi
129
+
71
130
  # Check for existing key
72
131
  KEY_FILE="deploy_key"
73
132
  if [[ -f "$KEY_FILE" ]] || [[ -f "${KEY_FILE}.pub" ]]; then
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "quality checks",
3
+ "target": "branch",
4
+ "enforcement": "active",
5
+ "conditions": {
6
+ "ref_name": {
7
+ "exclude": [],
8
+ "include": [
9
+ "~DEFAULT_BRANCH",
10
+ "refs/heads/dev",
11
+ "refs/heads/staging",
12
+ "refs/heads/main"
13
+ ]
14
+ }
15
+ },
16
+ "bypass_actors": [
17
+ {
18
+ "actor_id": null,
19
+ "actor_type": "DeployKey",
20
+ "bypass_mode": "always"
21
+ },
22
+ {
23
+ "actor_id": 5,
24
+ "actor_type": "RepositoryRole",
25
+ "bypass_mode": "always"
26
+ }
27
+ ],
28
+ "rules": [
29
+ {
30
+ "type": "required_status_checks",
31
+ "parameters": {
32
+ "strict_required_status_checks_policy": false,
33
+ "do_not_enforce_on_create": true,
34
+ "required_status_checks": [
35
+ {
36
+ "context": "🔍 Quality Checks / 🧹 Lint",
37
+ "integration_id": 15368
38
+ },
39
+ {
40
+ "context": "🔍 Quality Checks / 🔍 Type Check",
41
+ "integration_id": 15368
42
+ },
43
+ {
44
+ "context": "🔍 Quality Checks / 🏗️ Build",
45
+ "integration_id": 15368
46
+ },
47
+ {
48
+ "context": "🔍 Quality Checks / 📐 Check Formatting",
49
+ "integration_id": 15368
50
+ },
51
+ {
52
+ "context": "🔍 Quality Checks / 🔒 Security Scan",
53
+ "integration_id": 15368
54
+ },
55
+ {
56
+ "context": "🔍 Quality Checks / 🧪 Run Unit Tests",
57
+ "integration_id": 15368
58
+ },
59
+ {
60
+ "context": "🔍 Quality Checks / 🧪 Run Integration Tests",
61
+ "integration_id": 15368
62
+ }
63
+ ]
64
+ }
65
+ }
66
+ ]
67
+ }