neon_sakura 0.1.4

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 (251) hide show
  1. checksums.yaml +7 -0
  2. data/.ai-reviewer/README.md +182 -0
  3. data/.ai-reviewer/ai-reviewer.sh +56 -0
  4. data/.ai-reviewer/build-system-prompt.sh +136 -0
  5. data/.ai-reviewer/extract-claude-sections.sh +32 -0
  6. data/.ai-reviewer/test-ai-reviewer.sh +40 -0
  7. data/.ai-reviewer-config.yml +190 -0
  8. data/.github/dependabot.yml +12 -0
  9. data/.github/settings.yml +70 -0
  10. data/.github/workflows/ai-pr-review-on-comment.yml +384 -0
  11. data/.github/workflows/ai-pr-review.yml +328 -0
  12. data/.github/workflows/license-check.yml +78 -0
  13. data/.github/workflows/lint.yml +79 -0
  14. data/.github/workflows/security.yml +131 -0
  15. data/.github/workflows/semgrep.yml +26 -0
  16. data/.github/workflows/test.yml +44 -0
  17. data/.gitignore +75 -0
  18. data/.rubocop.yml +33 -0
  19. data/.ruby-version +1 -0
  20. data/.simplecov +14 -0
  21. data/.stylelintignore +10 -0
  22. data/.stylelintrc.json +37 -0
  23. data/AGENTS.md +51 -0
  24. data/CHANGELOG.md +568 -0
  25. data/CLAUDE.md +632 -0
  26. data/Gemfile +8 -0
  27. data/Gemfile.lock +327 -0
  28. data/LICENSE +21 -0
  29. data/README.md +1209 -0
  30. data/Rakefile +25 -0
  31. data/app/assets/images/cherry_blossom.svg +1525 -0
  32. data/app/assets/images/cherry_blossom_tree.png +0 -0
  33. data/app/assets/images/prysm-icon.png +0 -0
  34. data/app/assets/stylesheets/base.css +29 -0
  35. data/app/assets/stylesheets/components.css +1652 -0
  36. data/app/assets/stylesheets/forms.css +152 -0
  37. data/app/assets/stylesheets/loading.css +145 -0
  38. data/app/assets/stylesheets/neon_sakura.css +40 -0
  39. data/app/assets/stylesheets/pagy-tailwind.css +120 -0
  40. data/app/assets/stylesheets/theme-default.css +40 -0
  41. data/app/assets/stylesheets/theme-green.css +84 -0
  42. data/app/assets/stylesheets/theme-purple.css +94 -0
  43. data/app/assets/stylesheets/theme-red.css +84 -0
  44. data/app/assets/stylesheets/utility-borders.css +29 -0
  45. data/app/assets/stylesheets/utility-colors.css +185 -0
  46. data/app/assets/stylesheets/utility-effects.css +123 -0
  47. data/app/assets/stylesheets/utility-gradients.css +158 -0
  48. data/app/assets/stylesheets/utility-layout.css +132 -0
  49. data/app/assets/stylesheets/utility-reset.css +13 -0
  50. data/app/assets/stylesheets/utility-responsive.css +145 -0
  51. data/app/assets/stylesheets/utility-sizing.css +99 -0
  52. data/app/assets/stylesheets/utility-spacing.css +174 -0
  53. data/app/assets/stylesheets/utility-typography.css +97 -0
  54. data/app/controllers/errors_controller.rb +120 -0
  55. data/app/controllers/style_guide_controller.rb +117 -0
  56. data/app/helpers/errors_helper.rb +12 -0
  57. data/app/helpers/neon_sakura/navbar_helper.rb +43 -0
  58. data/app/helpers/style_guide_helper.rb +36 -0
  59. data/app/javascript/neon_sakura/dropdown.js +22 -0
  60. data/app/javascript/neon_sakura/navbar.js +71 -0
  61. data/app/javascript/neon_sakura/theme_switcher.js +187 -0
  62. data/app/views/errors/show.html.erb +105 -0
  63. data/app/views/layouts/error.html.erb +19 -0
  64. data/app/views/layouts/mission_control/jobs/_application_selection.html.erb +14 -0
  65. data/app/views/layouts/mission_control/jobs/_navigation.html.erb +21 -0
  66. data/app/views/layouts/mission_control/jobs/application.html.erb +453 -0
  67. data/app/views/layouts/style_guide.html.erb +416 -0
  68. data/app/views/shared/_file_upload.html.erb +184 -0
  69. data/app/views/shared/_footer.html.erb +23 -0
  70. data/app/views/shared/_header.html.erb +42 -0
  71. data/app/views/shared/_navbar.html.erb +306 -0
  72. data/app/views/shared/_profile_image_selector.html.erb +165 -0
  73. data/app/views/shared/_theme_switcher.html.erb +64 -0
  74. data/app/views/shared/icons/_adjustments.html.erb +10 -0
  75. data/app/views/shared/icons/_alert_circle.html.erb +3 -0
  76. data/app/views/shared/icons/_alert_triangle.html.erb +3 -0
  77. data/app/views/shared/icons/_archive.html.erb +3 -0
  78. data/app/views/shared/icons/_arrow_down.html.erb +3 -0
  79. data/app/views/shared/icons/_arrow_left.html.erb +3 -0
  80. data/app/views/shared/icons/_arrow_up.html.erb +3 -0
  81. data/app/views/shared/icons/_arrows_pointing_in.html.erb +10 -0
  82. data/app/views/shared/icons/_arrows_pointing_out.html.erb +10 -0
  83. data/app/views/shared/icons/_artemis_logo.html.erb +26 -0
  84. data/app/views/shared/icons/_auth_banner.html.erb +1 -0
  85. data/app/views/shared/icons/_bars.html.erb +10 -0
  86. data/app/views/shared/icons/_bell.html.erb +3 -0
  87. data/app/views/shared/icons/_book.html.erb +3 -0
  88. data/app/views/shared/icons/_bookmark.html.erb +3 -0
  89. data/app/views/shared/icons/_box.html.erb +3 -0
  90. data/app/views/shared/icons/_brain.html.erb +3 -0
  91. data/app/views/shared/icons/_briefcase.html.erb +3 -0
  92. data/app/views/shared/icons/_calendar.html.erb +3 -0
  93. data/app/views/shared/icons/_camera.html.erb +4 -0
  94. data/app/views/shared/icons/_chart_bar.html.erb +3 -0
  95. data/app/views/shared/icons/_chart_line.html.erb +10 -0
  96. data/app/views/shared/icons/_chart_pie.html.erb +11 -0
  97. data/app/views/shared/icons/_chat.html.erb +3 -0
  98. data/app/views/shared/icons/_check.html.erb +3 -0
  99. data/app/views/shared/icons/_check_circle.html.erb +3 -0
  100. data/app/views/shared/icons/_cherry_blossom.html.erb +1516 -0
  101. data/app/views/shared/icons/_cherry_blossom_silhouette.html.erb +1016 -0
  102. data/app/views/shared/icons/_cherry_blossom_single_flower.html.erb +1125 -0
  103. data/app/views/shared/icons/_cherry_blossom_tree.html.erb +159 -0
  104. data/app/views/shared/icons/_chevron_down.html.erb +3 -0
  105. data/app/views/shared/icons/_chevron_right.html.erb +9 -0
  106. data/app/views/shared/icons/_clipboard.html.erb +3 -0
  107. data/app/views/shared/icons/_clock.html.erb +3 -0
  108. data/app/views/shared/icons/_close.html.erb +3 -0
  109. data/app/views/shared/icons/_cog.html.erb +4 -0
  110. data/app/views/shared/icons/_crop.html.erb +10 -0
  111. data/app/views/shared/icons/_crown.html.erb +3 -0
  112. data/app/views/shared/icons/_disc.html.erb +3 -0
  113. data/app/views/shared/icons/_download.html.erb +3 -0
  114. data/app/views/shared/icons/_dragonfly.html.erb +58 -0
  115. data/app/views/shared/icons/_duplicate.html.erb +4 -0
  116. data/app/views/shared/icons/_edit.html.erb +3 -0
  117. data/app/views/shared/icons/_envelope.html.erb +3 -0
  118. data/app/views/shared/icons/_eraser.html.erb +10 -0
  119. data/app/views/shared/icons/_external_link.html.erb +3 -0
  120. data/app/views/shared/icons/_eye.html.erb +4 -0
  121. data/app/views/shared/icons/_file_csv.html.erb +10 -0
  122. data/app/views/shared/icons/_file_export.html.erb +10 -0
  123. data/app/views/shared/icons/_file_image.html.erb +10 -0
  124. data/app/views/shared/icons/_file_import.html.erb +10 -0
  125. data/app/views/shared/icons/_file_question.html.erb +6 -0
  126. data/app/views/shared/icons/_film.html.erb +3 -0
  127. data/app/views/shared/icons/_filter.html.erb +3 -0
  128. data/app/views/shared/icons/_folder.html.erb +3 -0
  129. data/app/views/shared/icons/_folder_open.html.erb +3 -0
  130. data/app/views/shared/icons/_folder_plus.html.erb +3 -0
  131. data/app/views/shared/icons/_globe.html.erb +3 -0
  132. data/app/views/shared/icons/_google.html.erb +11 -0
  133. data/app/views/shared/icons/_heart.html.erb +3 -0
  134. data/app/views/shared/icons/_heart_broken.html.erb +11 -0
  135. data/app/views/shared/icons/_heart_pulse.html.erb +4 -0
  136. data/app/views/shared/icons/_history.html.erb +11 -0
  137. data/app/views/shared/icons/_home.html.erb +10 -0
  138. data/app/views/shared/icons/_image.html.erb +3 -0
  139. data/app/views/shared/icons/_inbox.html.erb +3 -0
  140. data/app/views/shared/icons/_info_circle.html.erb +10 -0
  141. data/app/views/shared/icons/_key.html.erb +3 -0
  142. data/app/views/shared/icons/_layers.html.erb +10 -0
  143. data/app/views/shared/icons/_lightbulb.html.erb +10 -0
  144. data/app/views/shared/icons/_lightning.html.erb +3 -0
  145. data/app/views/shared/icons/_list.html.erb +3 -0
  146. data/app/views/shared/icons/_lock.html.erb +3 -0
  147. data/app/views/shared/icons/_logout.html.erb +3 -0
  148. data/app/views/shared/icons/_magazine.html.erb +3 -0
  149. data/app/views/shared/icons/_magic.html.erb +3 -0
  150. data/app/views/shared/icons/_minus.html.erb +10 -0
  151. data/app/views/shared/icons/_mobile.html.erb +10 -0
  152. data/app/views/shared/icons/_moon.html.erb +3 -0
  153. data/app/views/shared/icons/_network.html.erb +10 -0
  154. data/app/views/shared/icons/_new_item_banner.html.erb +1 -0
  155. data/app/views/shared/icons/_ouroboros.html.erb +24 -0
  156. data/app/views/shared/icons/_package.html.erb +3 -0
  157. data/app/views/shared/icons/_palette.html.erb +3 -0
  158. data/app/views/shared/icons/_paper_plane.html.erb +10 -0
  159. data/app/views/shared/icons/_photo.html.erb +10 -0
  160. data/app/views/shared/icons/_play.html.erb +4 -0
  161. data/app/views/shared/icons/_plus.html.erb +3 -0
  162. data/app/views/shared/icons/_pocket.html.erb +11 -0
  163. data/app/views/shared/icons/_prysm-icon.html.erb +34 -0
  164. data/app/views/shared/icons/_prysm.html.erb +13 -0
  165. data/app/views/shared/icons/_pushbullet-1.html.erb +29 -0
  166. data/app/views/shared/icons/_pushbullet-2.html.erb +2 -0
  167. data/app/views/shared/icons/_puzzle.html.erb +10 -0
  168. data/app/views/shared/icons/_qrcode.html.erb +3 -0
  169. data/app/views/shared/icons/_question.html.erb +3 -0
  170. data/app/views/shared/icons/_receipt.html.erb +10 -0
  171. data/app/views/shared/icons/_redo.html.erb +3 -0
  172. data/app/views/shared/icons/_refresh.html.erb +3 -0
  173. data/app/views/shared/icons/_rocket.html.erb +10 -0
  174. data/app/views/shared/icons/_rss.html.erb +3 -0
  175. data/app/views/shared/icons/_save.html.erb +3 -0
  176. data/app/views/shared/icons/_search.html.erb +3 -0
  177. data/app/views/shared/icons/_search_minus.html.erb +10 -0
  178. data/app/views/shared/icons/_search_plus.html.erb +10 -0
  179. data/app/views/shared/icons/_server_error.html.erb +6 -0
  180. data/app/views/shared/icons/_share.html.erb +3 -0
  181. data/app/views/shared/icons/_shield_check.html.erb +3 -0
  182. data/app/views/shared/icons/_sign_in.html.erb +3 -0
  183. data/app/views/shared/icons/_spinner.html.erb +4 -0
  184. data/app/views/shared/icons/_star.html.erb +3 -0
  185. data/app/views/shared/icons/_store.html.erb +10 -0
  186. data/app/views/shared/icons/_sun.html.erb +3 -0
  187. data/app/views/shared/icons/_sync.html.erb +3 -0
  188. data/app/views/shared/icons/_table.html.erb +3 -0
  189. data/app/views/shared/icons/_tag.html.erb +3 -0
  190. data/app/views/shared/icons/_tags.html.erb +11 -0
  191. data/app/views/shared/icons/_tools.html.erb +4 -0
  192. data/app/views/shared/icons/_trash.html.erb +3 -0
  193. data/app/views/shared/icons/_undo.html.erb +3 -0
  194. data/app/views/shared/icons/_unlock.html.erb +3 -0
  195. data/app/views/shared/icons/_upload.html.erb +3 -0
  196. data/app/views/shared/icons/_user.html.erb +3 -0
  197. data/app/views/shared/icons/_user_circle.html.erb +10 -0
  198. data/app/views/shared/icons/_user_plus.html.erb +10 -0
  199. data/app/views/shared/icons/_video.html.erb +3 -0
  200. data/app/views/shared/icons/_wrench.html.erb +11 -0
  201. data/app/views/style_guide/index.html.erb +77 -0
  202. data/app/views/style_guide/sections/_alerts.html.erb +114 -0
  203. data/app/views/style_guide/sections/_badges.html.erb +78 -0
  204. data/app/views/style_guide/sections/_buttons.html.erb +130 -0
  205. data/app/views/style_guide/sections/_cards.html.erb +84 -0
  206. data/app/views/style_guide/sections/_colors.html.erb +106 -0
  207. data/app/views/style_guide/sections/_file_upload.html.erb +135 -0
  208. data/app/views/style_guide/sections/_forms.html.erb +129 -0
  209. data/app/views/style_guide/sections/_gradients.html.erb +253 -0
  210. data/app/views/style_guide/sections/_header.html.erb +12 -0
  211. data/app/views/style_guide/sections/_icons.html.erb +55 -0
  212. data/app/views/style_guide/sections/_images.html.erb +40 -0
  213. data/app/views/style_guide/sections/_loading.html.erb +242 -0
  214. data/app/views/style_guide/sections/_pagination.html.erb +212 -0
  215. data/app/views/style_guide/sections/_profile_components.html.erb +203 -0
  216. data/app/views/style_guide/sections/_theme_switcher.html.erb +72 -0
  217. data/app/views/style_guide/sections/_typography.html.erb +65 -0
  218. data/bin/ai-optimize-claude-md +540 -0
  219. data/bin/ai-review-local +345 -0
  220. data/bin/ai-security-review +585 -0
  221. data/bin/brakeman +9 -0
  222. data/bin/install-hooks +57 -0
  223. data/bin/rake +7 -0
  224. data/bin/rubocop +10 -0
  225. data/bin/verify_setup.rb +31 -0
  226. data/config/brakeman.ignore +28 -0
  227. data/config/initializers/neon_sakura.rb +15 -0
  228. data/config/license_overrides.yml +13 -0
  229. data/config/routes.rb +21 -0
  230. data/config/theme_mappings.yml +61 -0
  231. data/docs/PRYSM_ASSETS.md +210 -0
  232. data/docs/plans/extract_ai_reviewer_plan.md +151 -0
  233. data/docs/plans/neon_sakura_gem_plan.md +138 -0
  234. data/lib/neon_sakura/configuration.rb +94 -0
  235. data/lib/neon_sakura/engine.rb +48 -0
  236. data/lib/neon_sakura/icon_helper.rb +54 -0
  237. data/lib/neon_sakura/profile_helper.rb +24 -0
  238. data/lib/neon_sakura/stylesheet_helper.rb +40 -0
  239. data/lib/neon_sakura/theme_helper.rb +63 -0
  240. data/lib/neon_sakura/theme_importer.rb +112 -0
  241. data/lib/neon_sakura/version.rb +5 -0
  242. data/lib/neon_sakura.rb +13 -0
  243. data/neon_sakura.gemspec +50 -0
  244. data/package.json +18 -0
  245. data/scripts/git-hooks/post-merge +132 -0
  246. data/scripts/git-hooks/pre-commit +123 -0
  247. data/scripts/git-hooks/pre-push +127 -0
  248. data/scripts/license-check.rb +587 -0
  249. data/settings.local.json +12 -0
  250. data/yarn.lock +778 -0
  251. metadata +503 -0
@@ -0,0 +1,70 @@
1
+ # GitHub Repository Settings
2
+ # This file can be used with the Probot Settings app: https://github.com/apps/settings
3
+ # Or manually configure these settings in your repository
4
+
5
+ repository:
6
+ # Repository name
7
+ name: neon_sakura
8
+
9
+ # Repository description
10
+ description: A Rails 8 application with advanced search and download capabilities
11
+
12
+ # Repository homepage
13
+ homepage: https://github.com/TRex22/neon_sakura
14
+
15
+ # Repository topics
16
+ topics: ruby, rails, rails8, yjit, sqlite, solidqueue
17
+
18
+ # Either true to enable automated security fixes, or false to disable
19
+ enable_automated_security_fixes: true
20
+
21
+ # Either true to enable vulnerability alerts, or false to disable
22
+ enable_vulnerability_alerts: true
23
+
24
+ # Collaborators: none configured (manage via GitHub UI)
25
+
26
+ # See https://docs.github.com/en/rest/reference/repos#update-a-repository for all available settings
27
+
28
+ # Branch protection rules
29
+ branches:
30
+ - name: main
31
+ # Required. Require at least one approving review on a pull request, before merging.
32
+ protection:
33
+ # Required. Require status checks to pass before merging.
34
+ required_status_checks:
35
+ # Required. Require branches to be up to date before merging.
36
+ strict: true
37
+ # Required. The list of status checks to require in order to merge into this branch
38
+ contexts:
39
+ - scan_ruby
40
+ - scan_js
41
+ - lint
42
+ - test
43
+ - system-test
44
+
45
+ # Required. Enforce all configured restrictions for administrators.
46
+ enforce_admins: false
47
+
48
+ # Required. Require at least one approving review on a pull request, before merging.
49
+ required_pull_request_reviews:
50
+ # Specify if approved reviews should be dismissed when new commits are pushed
51
+ dismiss_stale_reviews: true
52
+ # Specify if new reviewable commits should dismiss approved reviews
53
+ require_code_owner_reviews: false
54
+ # Specify the number of reviewers required to approve pull requests
55
+ required_approving_review_count: 0
56
+
57
+ # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable.
58
+ restrictions: null
59
+
60
+ # Required. Require linear history
61
+ required_linear_history: false
62
+
63
+ # Permits force pushes to the protected branch by anyone with write access to the repository.
64
+ allow_force_pushes: false
65
+
66
+ # Allows deletion of the protected branch by anyone with write access to the repository.
67
+ allow_deletions: false
68
+
69
+ # Required. Allow specified users and teams to bypass required pull requests
70
+ required_conversation_resolution: true
@@ -0,0 +1,384 @@
1
+ name: AI PR Review on Comment
2
+
3
+ on:
4
+ issue_comment:
5
+ types: [created]
6
+
7
+ permissions:
8
+ contents: read
9
+ pull-requests: write
10
+ issues: write
11
+ models: read
12
+
13
+ jobs:
14
+ ai-review-on-comment:
15
+ # Only run on PR comments that mention the bot
16
+ if: |
17
+ github.event.issue.pull_request &&
18
+ (contains(github.event.comment.body, '@ai-review') ||
19
+ contains(github.event.comment.body, '/review'))
20
+ runs-on: ubuntu-latest
21
+
22
+ steps:
23
+ - name: React to comment
24
+ uses: actions/github-script@v8
25
+ with:
26
+ github-token: ${{ secrets.GITHUB_TOKEN }}
27
+ script: |
28
+ await github.rest.reactions.createForIssueComment({
29
+ owner: context.repo.owner,
30
+ repo: context.repo.repo,
31
+ comment_id: context.payload.comment.id,
32
+ content: 'rocket'
33
+ });
34
+
35
+ - name: Get PR details
36
+ id: pr-details
37
+ uses: actions/github-script@v8
38
+ with:
39
+ github-token: ${{ secrets.GITHUB_TOKEN }}
40
+ script: |
41
+ const pr = await github.rest.pulls.get({
42
+ owner: context.repo.owner,
43
+ repo: context.repo.repo,
44
+ pull_number: context.issue.number
45
+ });
46
+
47
+ core.setOutput('base_ref', pr.data.base.ref);
48
+ core.setOutput('head_ref', pr.data.head.ref);
49
+ core.setOutput('head_sha', pr.data.head.sha);
50
+ core.setOutput('pr_title', pr.data.title);
51
+ core.setOutput('pr_body', pr.data.body || '');
52
+ return pr.data;
53
+
54
+ - name: Checkout PR code
55
+ uses: actions/checkout@v6
56
+ with:
57
+ ref: ${{ steps.pr-details.outputs.head_sha }}
58
+ fetch-depth: 0
59
+
60
+ - name: Get previous AI reviews and comments
61
+ id: previous-reviews
62
+ uses: actions/github-script@v8
63
+ with:
64
+ github-token: ${{ secrets.GITHUB_TOKEN }}
65
+ script: |
66
+ // Fetch both PR comments and review comments
67
+ const comments = await github.rest.issues.listComments({
68
+ owner: context.repo.owner,
69
+ repo: context.repo.repo,
70
+ issue_number: context.issue.number
71
+ });
72
+
73
+ const aiComments = comments.data
74
+ .filter(c => c.body.includes('🤖 AI Code Review'))
75
+ .map(c => ({
76
+ type: 'comment',
77
+ created_at: c.created_at,
78
+ body: c.body
79
+ }));
80
+
81
+ const reviews = await github.rest.pulls.listReviews({
82
+ owner: context.repo.owner,
83
+ repo: context.repo.repo,
84
+ pull_number: context.issue.number
85
+ });
86
+
87
+ const aiReviews = reviews.data
88
+ .filter(r => r.body && r.body.includes('🤖 AI Code Review'))
89
+ .map(r => ({
90
+ type: 'review',
91
+ created_at: r.submitted_at,
92
+ state: r.state,
93
+ body: r.body
94
+ }));
95
+
96
+ // Combine and sort by date (newest last)
97
+ const allReviews = [...aiComments, ...aiReviews]
98
+ .sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
99
+ .slice(-3) // Last 3 reviews/comments
100
+ .map(r => r.body)
101
+ .join('\n---\n');
102
+
103
+ const truncated = allReviews.substring(0, 3000);
104
+
105
+ core.setOutput('reviews', truncated);
106
+ core.setOutput('has_reviews', truncated.length > 0 ? 'true' : 'false');
107
+
108
+ - name: Get PR diff
109
+ id: pr-diff
110
+ env:
111
+ BASE_REF: ${{ steps.pr-details.outputs.base_ref }}
112
+ run: |
113
+ git fetch origin "$BASE_REF"
114
+
115
+ DIFF=$(git diff "origin/$BASE_REF...HEAD")
116
+
117
+ # Filter out ignored sections
118
+ echo "$DIFF" | awk '
119
+ /ai-review-ignore-start/ { ignore=1; next }
120
+ /ai-review-ignore-end/ { ignore=0; next }
121
+ !ignore { print }
122
+ ' > pr_diff_filtered.txt
123
+
124
+ DIFF=$(cat pr_diff_filtered.txt)
125
+
126
+ DIFF_LENGTH=${#DIFF}
127
+ if [ $DIFF_LENGTH -gt 15000 ]; then
128
+ DIFF="${DIFF:0:15000}"
129
+ DIFF="$DIFF\n\n... (diff truncated due to size)"
130
+ fi
131
+
132
+ echo "$DIFF" > pr_diff.txt
133
+
134
+ - name: Get changed files list
135
+ id: changed-files
136
+ env:
137
+ BASE_REF: ${{ steps.pr-details.outputs.base_ref }}
138
+ run: |
139
+ git fetch origin "$BASE_REF"
140
+ FILES=$(git diff --name-only "origin/$BASE_REF...HEAD" | head -50)
141
+ echo "files<<EOF" >> $GITHUB_OUTPUT
142
+ echo "$FILES" >> $GITHUB_OUTPUT
143
+ echo "EOF" >> $GITHUB_OUTPUT
144
+
145
+ - name: Read project guidelines
146
+ id: guidelines
147
+ run: |
148
+ CLAUDE_MD=""
149
+ RUBOCOP_YML=""
150
+
151
+ # Extract comprehensive sections from CLAUDE.md using shared helper
152
+ if [ -f "CLAUDE.md" ] && [ -f ".ai-reviewer/extract-claude-sections.sh" ]; then
153
+ CLAUDE_MD=$(.ai-reviewer/extract-claude-sections.sh CLAUDE.md)
154
+ fi
155
+
156
+ # Only include key linting rules
157
+ if [ -f ".rubocop.yml" ]; then
158
+ RUBOCOP_YML=$(cat .rubocop.yml | grep -E "^[A-Z]|Enabled:|Max:" | head -c 1000)
159
+ fi
160
+
161
+ echo "claude_md<<EOF" >> $GITHUB_OUTPUT
162
+ echo "$CLAUDE_MD" >> $GITHUB_OUTPUT
163
+ echo "EOF" >> $GITHUB_OUTPUT
164
+
165
+ echo "rubocop_yml<<EOF" >> $GITHUB_OUTPUT
166
+ echo "$RUBOCOP_YML" >> $GITHUB_OUTPUT
167
+ echo "EOF" >> $GITHUB_OUTPUT
168
+
169
+ - name: Extract specific questions from comment
170
+ id: extract-question
171
+ uses: actions/github-script@v8
172
+ with:
173
+ script: |
174
+ const comment = context.payload.comment.body;
175
+
176
+ // Extract any text after the trigger
177
+ const triggers = ['@ai-review', '/review'];
178
+ let question = '';
179
+
180
+ for (const trigger of triggers) {
181
+ if (comment.includes(trigger)) {
182
+ const parts = comment.split(trigger);
183
+ if (parts.length > 1) {
184
+ question = parts[1].trim();
185
+ break;
186
+ }
187
+ }
188
+ }
189
+
190
+ core.setOutput('question', question);
191
+ return question;
192
+
193
+ - name: AI Code Review with Codestral
194
+ id: ai-review
195
+ env:
196
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
197
+ PR_TITLE: ${{ steps.pr-details.outputs.pr_title }}
198
+ PR_BODY: ${{ steps.pr-details.outputs.pr_body }}
199
+ CHANGED_FILES: ${{ steps.changed-files.outputs.files }}
200
+ CLAUDE_MD: ${{ steps.guidelines.outputs.claude_md }}
201
+ RUBOCOP_YML: ${{ steps.guidelines.outputs.rubocop_yml }}
202
+ USER_QUESTION: ${{ steps.extract-question.outputs.question }}
203
+ PREVIOUS_REVIEWS: ${{ steps.previous-reviews.outputs.reviews }}
204
+ HAS_PREVIOUS_REVIEWS: ${{ steps.previous-reviews.outputs.has_reviews }}
205
+ run: |
206
+ DIFF=$(cat pr_diff.txt)
207
+
208
+ # Use shared system prompt (source of truth)
209
+ if [ -f ".ai-reviewer/build-system-prompt.sh" ]; then
210
+ .ai-reviewer/build-system-prompt.sh > system.txt
211
+ else
212
+ echo "Error: Shared system prompt script not found" >&2
213
+ exit 1
214
+ fi
215
+
216
+ # Build user prompt
217
+ echo "**PR:** $PR_TITLE" > user.txt
218
+ echo "" >> user.txt
219
+ if [ -n "$PR_BODY" ]; then
220
+ echo "**Desc:** ${PR_BODY:0:500}" >> user.txt
221
+ echo "" >> user.txt
222
+ fi
223
+
224
+ # Add specific question prominently if provided
225
+ if [ -n "$USER_QUESTION" ]; then
226
+ echo "**USER QUESTION:** $USER_QUESTION" >> user.txt
227
+ echo "" >> user.txt
228
+ fi
229
+
230
+ echo "**Files:** $CHANGED_FILES" >> user.txt
231
+ echo "" >> user.txt
232
+ echo "**Diff:**" >> user.txt
233
+ echo '```diff' >> user.txt
234
+ echo "$DIFF" >> user.txt
235
+ echo '```' >> user.txt
236
+
237
+ # Add previous reviews if they exist
238
+ if [ "$HAS_PREVIOUS_REVIEWS" = "true" ]; then
239
+ echo "" >> user.txt
240
+ echo "**Previous Reviews:**" >> user.txt
241
+ echo "$PREVIOUS_REVIEWS" >> user.txt
242
+ echo "" >> user.txt
243
+ echo "Focus on: 1) Were previous issues fixed? 2) New changes since last review" >> user.txt
244
+ fi
245
+
246
+ # Only add guidelines if no previous reviews (first review needs full context)
247
+ if [ "$HAS_PREVIOUS_REVIEWS" != "true" ] && [ -n "$CLAUDE_MD" ]; then
248
+ echo "" >> user.txt
249
+ echo "**Project Guidelines (CLAUDE.md):**" >> user.txt
250
+ echo "$CLAUDE_MD" >> user.txt
251
+ fi
252
+
253
+ # Use jq to build properly escaped JSON from files
254
+ SYSTEM_PROMPT=$(cat system.txt)
255
+ USER_PROMPT=$(cat user.txt)
256
+
257
+ jq -n \
258
+ --arg system "$SYSTEM_PROMPT" \
259
+ --arg user "$USER_PROMPT" \
260
+ '{
261
+ messages: [
262
+ {role: "system", content: $system},
263
+ {role: "user", content: $user}
264
+ ],
265
+ model: "Codestral-2501",
266
+ temperature: 0.7,
267
+ max_tokens: 1500,
268
+ top_p: 0.95
269
+ }' > prompt.json
270
+
271
+ # Call GitHub Models API
272
+ RESPONSE=$(curl -s -X POST \
273
+ "https://models.inference.ai.azure.com/chat/completions" \
274
+ -H "Content-Type: application/json" \
275
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
276
+ -d @prompt.json)
277
+
278
+ # Extract review content
279
+ REVIEW=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // "Error: Unable to generate review"')
280
+
281
+ echo "$REVIEW" > review_raw.md
282
+
283
+ if echo "$REVIEW" | grep -q "Error:"; then
284
+ echo "ERROR: Failed to generate review"
285
+ echo "Response: $RESPONSE"
286
+ exit 1
287
+ fi
288
+
289
+ # Smart deduplication - removes both exact duplicates AND similar issues from same file:line
290
+ # This catches cases where AI generates variations like:
291
+ # - "file.rb:10 - Issue A"
292
+ # - "file.rb:10 - Issue B" <- should be deduplicated (same file:line)
293
+ # This allows reviewing config/docs files while preventing repetition
294
+ awk '
295
+ # Extract file:line pattern (e.g., "file.rb:10")
296
+ match($0, /^[[:space:]]*-[[:space:]]*`[^`]+:[0-9]+/) {
297
+ prefix = substr($0, RSTART, RLENGTH)
298
+ # If we have seen this file:line before, skip
299
+ if (seen[prefix]++) next
300
+ }
301
+ # Also skip exact duplicate lines
302
+ !seen_exact[$0]++
303
+ ' review_raw.md > review.md
304
+
305
+ # If review is still too long after deduplication, truncate it
306
+ if [ $(wc -l < review.md) -gt 100 ]; then
307
+ head -100 review.md > review_truncated.md
308
+ echo "" >> review_truncated.md
309
+ echo "... (review truncated - too many issues)" >> review_truncated.md
310
+ mv review_truncated.md review.md
311
+ fi
312
+
313
+ - name: Dismiss previous AI reviews
314
+ uses: actions/github-script@v8
315
+ with:
316
+ github-token: ${{ secrets.GITHUB_TOKEN }}
317
+ script: |
318
+ // Get all existing reviews for this PR
319
+ const reviews = await github.rest.pulls.listReviews({
320
+ owner: context.repo.owner,
321
+ repo: context.repo.repo,
322
+ pull_number: context.issue.number
323
+ });
324
+
325
+ // Find AI reviews that should be dismissed (REQUEST_CHANGES state)
326
+ const aiReviews = reviews.data.filter(r =>
327
+ r.body &&
328
+ r.body.includes('🤖 AI Code Review') &&
329
+ r.state === 'CHANGES_REQUESTED'
330
+ );
331
+
332
+ // Dismiss each AI review
333
+ for (const review of aiReviews) {
334
+ console.log(`Dismissing previous AI review #${review.id}`);
335
+ await github.rest.pulls.dismissReview({
336
+ owner: context.repo.owner,
337
+ repo: context.repo.repo,
338
+ pull_number: context.issue.number,
339
+ review_id: review.id,
340
+ message: 'New AI review requested - dismissing previous AI review'
341
+ });
342
+ }
343
+
344
+ - name: Post review as PR review (not comment)
345
+ uses: actions/github-script@v8
346
+ with:
347
+ github-token: ${{ secrets.GITHUB_TOKEN }}
348
+ script: |
349
+ const fs = require('fs');
350
+ const review = fs.readFileSync('review.md', 'utf8');
351
+
352
+ // Determine review event type based on the Verdict section only
353
+ // GitHub does not allow bots to APPROVE, so we use COMMENT or REQUEST_CHANGES
354
+ let event = 'COMMENT';
355
+ let verdict = '💬 COMMENT';
356
+
357
+ // Extract only the Verdict section to avoid false positives from policy text
358
+ // Look for the first line after "## Verdict" that contains the status
359
+ const verdictMatch = review.match(/## Verdict\s*\n\*\*Status:\*\*\s*([^\n]*)/i);
360
+ if (verdictMatch) {
361
+ const verdictLine = verdictMatch[1].trim();
362
+ if (verdictLine.includes('REQUEST_CHANGES') || verdictLine.includes('REQUEST CHANGES')) {
363
+ event = 'REQUEST_CHANGES';
364
+ verdict = '⚠️ REQUEST CHANGES';
365
+ }
366
+ }
367
+
368
+ const body = `## 🤖 AI Code Review - ${verdict}\n\n${review}\n\n---\n*Powered by GitHub Models (Codestral-2501) • Requested by @${context.payload.comment.user.login} • AI recommendations only, human approval required • [Ignore sections](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/docs/AI_PR_REVIEWER.md#ignoring-code-sections) with \`ai-review-ignore-start/end\`*`;
369
+
370
+ // Post as a proper PR review (not an issue comment)
371
+ // This prevents duplication and integrates better with GitHub's review system
372
+ await github.rest.pulls.createReview({
373
+ owner: context.repo.owner,
374
+ repo: context.repo.repo,
375
+ pull_number: context.issue.number,
376
+ body: body,
377
+ event: event
378
+ });
379
+
380
+ - name: Check for critical issues
381
+ run: |
382
+ if grep -qi "REQUEST CHANGES\|CRITICAL\|SECURITY.*CONCERN" review.md; then
383
+ echo "::warning::AI reviewer found critical issues that need attention"
384
+ fi