@codyswann/lisa 2.195.8 → 2.196.1

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 (78) 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/expo/github-rulesets/playwright.json +38 -0
  5. package/package.json +2 -2
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-setup-github-repo/SKILL.md +66 -0
  9. package/plugins/lisa/.codex-plugin/skills/lisa-setup-github-repo/agents/openai.yaml +4 -0
  10. package/plugins/lisa/commands/lisa/setup/github-repo.md +7 -0
  11. package/plugins/lisa/skills/lisa-setup-github-repo/SKILL.md +66 -0
  12. package/plugins/lisa/skills/lisa-setup-github-repo/agents/openai.yaml +4 -0
  13. package/plugins/lisa-agy/commands/lisa/setup/github-repo.md +7 -0
  14. package/plugins/lisa-agy/plugin.json +1 -1
  15. package/plugins/lisa-agy/skills/lisa-setup-github-repo/SKILL.md +66 -0
  16. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  19. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/commands/lisa/setup/github-repo.md +7 -0
  23. package/plugins/lisa-copilot/skills/lisa-setup-github-repo/SKILL.md +66 -0
  24. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cursor/commands/lisa/setup/github-repo.md +7 -0
  26. package/plugins/lisa-cursor/skills/lisa-setup-github-repo/SKILL.md +66 -0
  27. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-agy/plugin.json +1 -1
  30. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  35. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  40. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  45. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  50. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails-agy/plugin.json +1 -1
  55. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  60. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  65. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  66. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  67. package/plugins/src/base/commands/lisa/setup/github-repo.md +7 -0
  68. package/plugins/src/base/skills/lisa-setup-github-repo/SKILL.md +66 -0
  69. package/rails/github-rulesets/quality-checks.json +51 -0
  70. package/scripts/cleanup-github-branches.sh +96 -72
  71. package/scripts/cleanup-local-merged-branches.sh +110 -0
  72. package/scripts/cleanup-worktrees.sh +222 -0
  73. package/scripts/lisa-github-repo-settings.sh +152 -0
  74. package/scripts/lisa-github-repo-setup.sh +73 -0
  75. package/scripts/lisa-github-rulesets.sh +612 -0
  76. package/scripts/setup-deploy-key.sh +62 -2
  77. package/typescript/github-rulesets/quality-checks.json +67 -0
  78. package/typescript/github-rulesets/base.json +0 -33
@@ -0,0 +1,612 @@
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
+ if [[ -n "$existing_id" ]]; then
348
+ log_info "Updating ruleset '$ruleset_name' (id: $existing_id)..."
349
+ else
350
+ log_info "Creating ruleset '$ruleset_name'..."
351
+ fi
352
+
353
+ if apply_with_integration_fallback "$repo" "$ruleset_name" "$clean_template" "$existing_id"; then
354
+ return 0
355
+ fi
356
+ log_error "Failed to apply ruleset '$ruleset_name'"
357
+ return 1
358
+ }
359
+
360
+ # Send the ruleset to GitHub. App-based required checks (CodeRabbit,
361
+ # GitGuardian, ...) are rejected with "Invalid integration ids" on repos
362
+ # where that app is not installed — and no user-token API can list per-repo
363
+ # installations up front. So on that specific error, retry with app-based
364
+ # checks progressively removed: first each single app id, then all of them,
365
+ # keeping as many checks as the repo actually supports.
366
+ apply_with_integration_fallback() {
367
+ local repo="$1"
368
+ local ruleset_name="$2"
369
+ local payload="$3"
370
+ local existing_id="$4"
371
+
372
+ local temp_file error_output
373
+ temp_file=$(mktemp)
374
+
375
+ # Capture stdout AND stderr — gh prints the API error body (which carries
376
+ # the "Invalid integration ids" detail) on stdout, not stderr.
377
+ send_ruleset() {
378
+ echo "$1" > "$temp_file"
379
+ if [[ -n "$existing_id" ]]; then
380
+ error_output=$(gh api -X PUT "repos/$repo/rulesets/$existing_id" --input "$temp_file" 2>&1)
381
+ else
382
+ error_output=$(gh api -X POST "repos/$repo/rulesets" --input "$temp_file" 2>&1)
383
+ fi
384
+ }
385
+
386
+ if send_ruleset "$payload"; then
387
+ log_success "Applied ruleset '$ruleset_name'"
388
+ rm -f "$temp_file"
389
+ return 0
390
+ fi
391
+
392
+ if ! echo "$error_output" | grep -qi "invalid integration ids"; then
393
+ log_error "$error_output"
394
+ rm -f "$temp_file"
395
+ return 1
396
+ fi
397
+
398
+ local app_ids
399
+ app_ids=$(echo "$payload" | jq --argjson actions "$ACTIONS_INTEGRATION_ID" \
400
+ '[.rules[]? | select(.type=="required_status_checks") | .parameters.required_status_checks[]?.integration_id | select(. != null and . != $actions)] | unique | .[]')
401
+
402
+ local drop_filter='
403
+ .rules |= (
404
+ map(
405
+ if .type == "required_status_checks" then
406
+ .parameters.required_status_checks |= map(select((.integration_id // -1) as $i | ($drop | index($i)) | not))
407
+ else . end
408
+ )
409
+ | map(select(.type != "required_status_checks" or (.parameters.required_status_checks | length) > 0))
410
+ )'
411
+
412
+ # Each single app id first (keep the most checks), then all app ids.
413
+ local candidates=()
414
+ local app_id
415
+ for app_id in $app_ids; do
416
+ candidates+=("[$app_id]")
417
+ done
418
+ candidates+=("$(echo "$app_ids" | jq -s -c .)")
419
+
420
+ local drop attempt
421
+ for drop in "${candidates[@]}"; do
422
+ attempt=$(echo "$payload" | jq --argjson drop "$drop" --argjson actions "$ACTIONS_INTEGRATION_ID" "$drop_filter")
423
+ if [[ $(echo "$attempt" | jq '(.rules // [1]) | length') -eq 0 ]]; then
424
+ continue
425
+ fi
426
+ if send_ruleset "$attempt"; then
427
+ log_warning "Applied ruleset '$ruleset_name' without app integration id(s) $drop — app(s) not installed on this repository"
428
+ rm -f "$temp_file"
429
+ return 0
430
+ fi
431
+ done
432
+
433
+ log_error "$error_output"
434
+ rm -f "$temp_file"
435
+ return 1
436
+ }
437
+
438
+ ##############################################################################
439
+ # Main Logic
440
+ ##############################################################################
441
+
442
+ collect_templates() {
443
+ local -a types=("$@")
444
+ local -a templates=()
445
+
446
+ # Always include 'all' first
447
+ local all_dir="$LISA_ROOT/all/github-rulesets"
448
+ if [[ -d "$all_dir" ]]; then
449
+ for file in "$all_dir"/*.json; do
450
+ [[ -f "$file" ]] && templates+=("$file")
451
+ done
452
+ fi
453
+
454
+ # Then add type-specific templates in order
455
+ for type in "${types[@]}"; do
456
+ local type_dir="$LISA_ROOT/$type/github-rulesets"
457
+ if [[ -d "$type_dir" ]]; then
458
+ for file in "$type_dir"/*.json; do
459
+ [[ -f "$file" ]] && templates+=("$file")
460
+ done
461
+ fi
462
+ done
463
+
464
+ echo "${templates[@]}"
465
+ }
466
+
467
+ main() {
468
+ # Parse arguments
469
+ while [[ $# -gt 0 ]]; do
470
+ case $1 in
471
+ -n|--dry-run)
472
+ DRY_RUN=true
473
+ shift
474
+ ;;
475
+ -y|--yes)
476
+ YES_MODE=true
477
+ shift
478
+ ;;
479
+ -v|--verbose)
480
+ VERBOSE=true
481
+ shift
482
+ ;;
483
+ -h|--help)
484
+ show_help
485
+ exit 0
486
+ ;;
487
+ -*)
488
+ log_error "Unknown option: $1"
489
+ show_help
490
+ exit 1
491
+ ;;
492
+ *)
493
+ PROJECT_PATH="$1"
494
+ shift
495
+ ;;
496
+ esac
497
+ done
498
+
499
+ # Default to current directory
500
+ PROJECT_PATH="${PROJECT_PATH:-.}"
501
+ PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
502
+
503
+ log_info "Lisa GitHub Rulesets"
504
+ echo ""
505
+
506
+ # Check prerequisites
507
+ check_prerequisites
508
+
509
+ # Get repository info
510
+ local repo
511
+ repo=$(get_repo_info "$PROJECT_PATH")
512
+ log_info "Repository: $repo"
513
+
514
+ # Detect project types
515
+ local detected_types_str
516
+ detected_types_str=$(detect_project_types "$PROJECT_PATH")
517
+
518
+ if [[ -z "$detected_types_str" ]]; then
519
+ log_warning "No specific project types detected, using 'all' templates only"
520
+ detected_types_str=""
521
+ fi
522
+
523
+ # Convert to array safely
524
+ local -a detected_types=()
525
+ if [[ -n "$detected_types_str" ]]; then
526
+ read -ra detected_types <<< "$detected_types_str"
527
+ fi
528
+
529
+ # Expand with parent types
530
+ local -a expanded_types=()
531
+ if [[ ${#detected_types[@]} -gt 0 ]]; then
532
+ local expanded_str
533
+ expanded_str=$(expand_types_with_parents "${detected_types[@]}")
534
+ read -ra expanded_types <<< "$expanded_str"
535
+ fi
536
+
537
+ if [[ ${#expanded_types[@]} -gt 0 ]]; then
538
+ log_info "Detected types: ${expanded_types[*]}"
539
+ fi
540
+
541
+ # Collect templates
542
+ local templates_str
543
+ templates_str=$(collect_templates "${expanded_types[@]}")
544
+
545
+ local -a templates=()
546
+ if [[ -n "$templates_str" ]]; then
547
+ read -ra templates <<< "$templates_str"
548
+ fi
549
+
550
+ if [[ ${#templates[@]} -eq 0 ]]; then
551
+ log_warning "No ruleset templates found"
552
+ exit 0
553
+ fi
554
+
555
+ log_info "Found ${#templates[@]} ruleset template(s)"
556
+ for t in "${templates[@]}"; do
557
+ log_verbose " - $t"
558
+ done
559
+ echo ""
560
+
561
+ # Confirmation prompt
562
+ if [[ "$DRY_RUN" == "false" ]] && [[ "$YES_MODE" == "false" ]]; then
563
+ echo -n "Apply rulesets to $repo? [y/N] "
564
+ read -r response
565
+ if [[ ! "$response" =~ ^[Yy]$ ]]; then
566
+ log_info "Aborted"
567
+ exit 0
568
+ fi
569
+ echo ""
570
+ fi
571
+
572
+ # Get existing rulesets — a 403 here means the plan doesn't support rulesets
573
+ # (private repo on a free personal plan); that's a skip, not a failure.
574
+ log_info "Fetching existing rulesets..."
575
+ local existing_rulesets
576
+ if ! existing_rulesets=$(gh api "repos/$repo/rulesets" 2>&1); then
577
+ if echo "$existing_rulesets" | grep -qi "upgrade to github\|HTTP 403"; then
578
+ log_warning "Rulesets are not available on this repository's plan — skipping"
579
+ exit 0
580
+ fi
581
+ log_error "Could not fetch rulesets: $existing_rulesets"
582
+ exit 1
583
+ fi
584
+ local existing_count
585
+ existing_count=$(echo "$existing_rulesets" | jq 'length')
586
+ log_verbose "Found $existing_count existing ruleset(s)"
587
+
588
+ # Apply each template
589
+ local success_count=0
590
+ local fail_count=0
591
+
592
+ for template in "${templates[@]}"; do
593
+ if apply_ruleset "$repo" "$template" "$existing_rulesets" "$PROJECT_PATH"; then
594
+ success_count=$((success_count + 1))
595
+ else
596
+ fail_count=$((fail_count + 1))
597
+ fi
598
+ done
599
+
600
+ echo ""
601
+ if [[ "$DRY_RUN" == "true" ]]; then
602
+ log_info "Dry run complete. ${#templates[@]} ruleset(s) would be applied."
603
+ else
604
+ log_success "Applied $success_count ruleset(s)"
605
+ if [[ $fail_count -gt 0 ]]; then
606
+ log_warning "$fail_count ruleset(s) failed"
607
+ exit 1
608
+ fi
609
+ fi
610
+ }
611
+
612
+ 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,54 @@ 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
+ # NOTE: gh's --jq flag does not support jq --arg; pipe through jq instead.
93
+ HAS_KEY=$(gh repo deploy-key list --repo "$OWNER/$REPO" --json title 2>/dev/null \
94
+ | jq --arg t "$KEY_TITLE" '[.[] | select(.title == $t)] | length' 2>/dev/null || echo 0)
95
+ HAS_SECRET=$(gh secret list --repo "$OWNER/$REPO" --json name \
96
+ --jq '[.[] | select(.name == "DEPLOY_KEY")] | length' 2>/dev/null || echo 0)
97
+ if [[ "$HAS_KEY" -gt 0 && "$HAS_SECRET" -gt 0 ]]; then
98
+ print_success "Deploy key and DEPLOY_KEY secret already configured — nothing to do."
99
+ exit 0
100
+ fi
101
+
102
+ TEMP_KEY_DIR=$(mktemp -d)
103
+ trap 'rm -rf "$TEMP_KEY_DIR"' EXIT
104
+ KEY_FILE="$TEMP_KEY_DIR/deploy_key"
105
+
106
+ print_step "Generating SSH key pair..."
107
+ ssh-keygen -t ed25519 -C "github-actions-deploy-key-${REPO}" -f "$KEY_FILE" -N "" -q
108
+
109
+ # A stale key under the same title blocks re-adding; replace it.
110
+ if [[ "$HAS_KEY" -gt 0 ]]; then
111
+ gh repo deploy-key list --repo "$OWNER/$REPO" --json id,title 2>/dev/null \
112
+ | jq -r --arg t "$KEY_TITLE" '.[] | select(.title == $t) | .id' \
113
+ | while read -r key_id; do
114
+ gh repo deploy-key delete "$key_id" --repo "$OWNER/$REPO" || true
115
+ done
116
+ fi
117
+
118
+ print_step "Adding deploy key to repository..."
119
+ gh repo deploy-key add "${KEY_FILE}.pub" --repo "$OWNER/$REPO" \
120
+ --title "$KEY_TITLE" --allow-write
121
+ print_success "Deploy key added with write access"
122
+
123
+ print_step "Adding DEPLOY_KEY secret..."
124
+ gh secret set DEPLOY_KEY --repo "$OWNER/$REPO" < "$KEY_FILE"
125
+ print_success "DEPLOY_KEY secret added"
126
+
127
+ print_success "Deploy key setup complete!"
128
+ exit 0
129
+ fi
130
+
71
131
  # Check for existing key
72
132
  KEY_FILE="deploy_key"
73
133
  if [[ -f "$KEY_FILE" ]] || [[ -f "${KEY_FILE}.pub" ]]; then