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,328 @@
1
+ name: AI PR Review
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened]
6
+
7
+ permissions:
8
+ contents: read
9
+ pull-requests: write
10
+ issues: write
11
+ models: read
12
+
13
+ jobs:
14
+ ai-review:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - name: Checkout code
19
+ uses: actions/checkout@v6
20
+ with:
21
+ fetch-depth: 0
22
+
23
+ - name: Get previous AI reviews and comments
24
+ id: previous-reviews
25
+ uses: actions/github-script@v8
26
+ with:
27
+ github-token: ${{ secrets.GITHUB_TOKEN }}
28
+ script: |
29
+ // Fetch both PR comments and review comments
30
+ const comments = await github.rest.issues.listComments({
31
+ owner: context.repo.owner,
32
+ repo: context.repo.repo,
33
+ issue_number: context.issue.number
34
+ });
35
+
36
+ const aiComments = comments.data
37
+ .filter(c => c.body.includes('🤖 AI Code Review'))
38
+ .map(c => ({
39
+ type: 'comment',
40
+ created_at: c.created_at,
41
+ body: c.body
42
+ }));
43
+
44
+ const reviews = await github.rest.pulls.listReviews({
45
+ owner: context.repo.owner,
46
+ repo: context.repo.repo,
47
+ pull_number: context.issue.number
48
+ });
49
+
50
+ const aiReviews = reviews.data
51
+ .filter(r => r.body && r.body.includes('🤖 AI Code Review'))
52
+ .map(r => ({
53
+ type: 'review',
54
+ created_at: r.submitted_at,
55
+ state: r.state,
56
+ body: r.body
57
+ }));
58
+
59
+ // Combine and sort by date (newest last)
60
+ const allReviews = [...aiComments, ...aiReviews]
61
+ .sort((a, b) => new Date(a.created_at) - new Date(b.created_at))
62
+ .slice(-3) // Last 3 reviews/comments
63
+ .map(r => r.body)
64
+ .join('\n---\n');
65
+
66
+ const truncated = allReviews.substring(0, 3000);
67
+
68
+ core.setOutput('reviews', truncated);
69
+ core.setOutput('has_reviews', truncated.length > 0 ? 'true' : 'false');
70
+
71
+ - name: Get PR diff
72
+ id: pr-diff
73
+ env:
74
+ BASE_REF: ${{ github.event.pull_request.base.ref }}
75
+ run: |
76
+ git fetch origin "$BASE_REF"
77
+ DIFF=$(git diff "origin/$BASE_REF...HEAD")
78
+
79
+ # Filter out ignored sections
80
+ # Remove lines between ai-review-ignore-start and ai-review-ignore-end
81
+ echo "$DIFF" | awk '
82
+ /ai-review-ignore-start/ { ignore=1; next }
83
+ /ai-review-ignore-end/ { ignore=0; next }
84
+ !ignore { print }
85
+ ' > pr_diff_filtered.txt
86
+
87
+ DIFF=$(cat pr_diff_filtered.txt)
88
+
89
+ DIFF_LENGTH=${#DIFF}
90
+ if [ $DIFF_LENGTH -gt 15000 ]; then
91
+ DIFF="${DIFF:0:15000}"
92
+ DIFF="$DIFF\n\n... (diff truncated due to size)"
93
+ fi
94
+
95
+ echo "$DIFF" > pr_diff.txt
96
+
97
+ - name: Get changed files list
98
+ id: changed-files
99
+ env:
100
+ BASE_REF: ${{ github.event.pull_request.base.ref }}
101
+ run: |
102
+ git fetch origin "$BASE_REF"
103
+ FILES=$(git diff --name-only "origin/$BASE_REF...HEAD" | head -50)
104
+ echo "files<<EOF" >> $GITHUB_OUTPUT
105
+ echo "$FILES" >> $GITHUB_OUTPUT
106
+ echo "EOF" >> $GITHUB_OUTPUT
107
+
108
+ - name: Read project guidelines
109
+ id: guidelines
110
+ run: |
111
+ CLAUDE_MD=""
112
+
113
+ # Extract comprehensive sections from CLAUDE.md using shared helper
114
+ # Only include on first review to save tokens
115
+ if [ -f "CLAUDE.md" ] && [ -f ".ai-reviewer/extract-claude-sections.sh" ]; then
116
+ CLAUDE_MD=$(.ai-reviewer/extract-claude-sections.sh CLAUDE.md)
117
+ fi
118
+
119
+ echo "claude_md<<EOF" >> $GITHUB_OUTPUT
120
+ echo "$CLAUDE_MD" >> $GITHUB_OUTPUT
121
+ echo "EOF" >> $GITHUB_OUTPUT
122
+
123
+ - name: Build prompts
124
+ env:
125
+ PR_TITLE: ${{ github.event.pull_request.title }}
126
+ PR_BODY: ${{ github.event.pull_request.body }}
127
+ CHANGED_FILES: ${{ steps.changed-files.outputs.files }}
128
+ CLAUDE_MD: ${{ steps.guidelines.outputs.claude_md }}
129
+ PREVIOUS_REVIEWS: ${{ steps.previous-reviews.outputs.reviews }}
130
+ HAS_PREVIOUS_REVIEWS: ${{ steps.previous-reviews.outputs.has_reviews }}
131
+ run: |
132
+ DIFF=$(cat pr_diff.txt)
133
+
134
+ # Use shared system prompt (source of truth)
135
+ if [ -f ".ai-reviewer/build-system-prompt.sh" ]; then
136
+ .ai-reviewer/build-system-prompt.sh > system.txt
137
+ else
138
+ echo "Error: Shared system prompt script not found" >&2
139
+ exit 1
140
+ fi
141
+
142
+ # Build user prompt - more concise to save tokens
143
+ echo "**PR:** $PR_TITLE" > user.txt
144
+ echo "" >> user.txt
145
+
146
+ # Add previous reviews if they exist (incremental review mode)
147
+ if [ "$HAS_PREVIOUS_REVIEWS" = "true" ]; then
148
+ echo "**Previous Reviews:**" >> user.txt
149
+ echo "$PREVIOUS_REVIEWS" >> user.txt
150
+ echo "" >> user.txt
151
+ echo "**Focus:** Review new changes only. Were previous issues fixed?" >> user.txt
152
+ echo "" >> user.txt
153
+ fi
154
+
155
+ # Add changed files count (not full list to save tokens)
156
+ FILE_COUNT=$(echo "$CHANGED_FILES" | wc -l)
157
+ echo "**Changed Files ($FILE_COUNT):**" >> user.txt
158
+ echo "$CHANGED_FILES" | head -20 >> user.txt
159
+ if [ $FILE_COUNT -gt 20 ]; then
160
+ echo "... and $((FILE_COUNT - 20)) more" >> user.txt
161
+ fi
162
+ echo "" >> user.txt
163
+
164
+ echo "**Diff:**" >> user.txt
165
+ echo '```diff' >> user.txt
166
+ echo "$DIFF" >> user.txt
167
+ echo '```' >> user.txt
168
+
169
+ # Only add guidelines on first review (token savings)
170
+ if [ "$HAS_PREVIOUS_REVIEWS" != "true" ] && [ -n "$CLAUDE_MD" ]; then
171
+ echo "" >> user.txt
172
+ echo "**Guidelines:**" >> user.txt
173
+ echo "$CLAUDE_MD" >> user.txt
174
+ fi
175
+
176
+ - name: AI Code Review with Codestral
177
+ id: ai-review
178
+ env:
179
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
180
+ run: |
181
+ # Use jq to build properly escaped JSON from files
182
+ SYSTEM_PROMPT=$(cat system.txt)
183
+ USER_PROMPT=$(cat user.txt)
184
+
185
+ # Reduce max_tokens to save costs (was 1500, now 1000)
186
+ # Increased temperature slightly for more concise responses
187
+ jq -n \
188
+ --arg system "$SYSTEM_PROMPT" \
189
+ --arg user "$USER_PROMPT" \
190
+ '{
191
+ messages: [
192
+ {role: "system", content: $system},
193
+ {role: "user", content: $user}
194
+ ],
195
+ model: "Codestral-2501",
196
+ temperature: 0.6,
197
+ max_tokens: 1000,
198
+ top_p: 0.9
199
+ }' > prompt.json
200
+
201
+ # Call GitHub Models API
202
+ RESPONSE=$(curl -s -X POST \
203
+ "https://models.inference.ai.azure.com/chat/completions" \
204
+ -H "Content-Type: application/json" \
205
+ -H "Authorization: Bearer $GITHUB_TOKEN" \
206
+ -d @prompt.json)
207
+
208
+ # Extract review content
209
+ REVIEW=$(echo "$RESPONSE" | jq -r '.choices[0].message.content // "Error: Unable to generate review"')
210
+
211
+ echo "$REVIEW" > review_raw.md
212
+
213
+ if echo "$REVIEW" | grep -q "Error:"; then
214
+ echo "ERROR: Failed to generate review"
215
+ echo "Response: $RESPONSE"
216
+ exit 1
217
+ fi
218
+
219
+ # Smart deduplication - removes both exact duplicates AND similar issues from same file:line
220
+ # This catches cases where AI generates variations like:
221
+ # - "file.rb:10 - Issue A"
222
+ # - "file.rb:10 - Issue B" <- should be deduplicated (same file:line)
223
+ # This allows reviewing config/docs files while preventing repetition
224
+ awk '
225
+ # Extract file:line pattern (e.g., "file.rb:10")
226
+ match($0, /^[[:space:]]*-[[:space:]]*`[^`]+:[0-9]+/) {
227
+ prefix = substr($0, RSTART, RLENGTH)
228
+ # If we have seen this file:line before, skip
229
+ if (seen[prefix]++) next
230
+ }
231
+ # Also skip exact duplicate lines
232
+ !seen_exact[$0]++
233
+ ' review_raw.md > review.md
234
+
235
+ # If review is still too long after deduplication, truncate it
236
+ if [ $(wc -l < review.md) -gt 100 ]; then
237
+ head -100 review.md > review_truncated.md
238
+ echo "" >> review_truncated.md
239
+ echo "... (review truncated - too many issues)" >> review_truncated.md
240
+ mv review_truncated.md review.md
241
+ fi
242
+
243
+ - name: Dismiss previous AI reviews
244
+ uses: actions/github-script@v8
245
+ with:
246
+ github-token: ${{ secrets.GITHUB_TOKEN }}
247
+ script: |
248
+ // Get all existing reviews for this PR
249
+ const reviews = await github.rest.pulls.listReviews({
250
+ owner: context.repo.owner,
251
+ repo: context.repo.repo,
252
+ pull_number: context.issue.number
253
+ });
254
+
255
+ // Find AI reviews that should be dismissed (REQUEST_CHANGES state)
256
+ const aiReviews = reviews.data.filter(r =>
257
+ r.body &&
258
+ r.body.includes('🤖 AI Code Review') &&
259
+ r.state === 'CHANGES_REQUESTED'
260
+ );
261
+
262
+ // Dismiss each AI review
263
+ for (const review of aiReviews) {
264
+ console.log(`Dismissing previous AI review #${review.id}`);
265
+ await github.rest.pulls.dismissReview({
266
+ owner: context.repo.owner,
267
+ repo: context.repo.repo,
268
+ pull_number: context.issue.number,
269
+ review_id: review.id,
270
+ message: 'New changes have been pushed - dismissing previous AI review'
271
+ });
272
+ }
273
+
274
+ - name: Post review as PR review (not comment)
275
+ uses: actions/github-script@v8
276
+ with:
277
+ github-token: ${{ secrets.GITHUB_TOKEN }}
278
+ script: |
279
+ const fs = require('fs');
280
+ const review = fs.readFileSync('review.md', 'utf8');
281
+
282
+ // Determine review event type based on the Verdict section only
283
+ // GitHub does not allow bots to APPROVE, so we use COMMENT or REQUEST_CHANGES
284
+ let event = 'COMMENT';
285
+ let verdict = '💬 COMMENT';
286
+
287
+ // Extract only the Verdict section to avoid false positives from policy text
288
+ // Look for the first line after "## Verdict" that contains the status
289
+ const verdictMatch = review.match(/## Verdict\s*\n\*\*Status:\*\*\s*([^\n]*)/i);
290
+ if (verdictMatch) {
291
+ const verdictLine = verdictMatch[1].trim();
292
+ if (verdictLine.includes('REQUEST_CHANGES') || verdictLine.includes('REQUEST CHANGES')) {
293
+ event = 'REQUEST_CHANGES';
294
+ verdict = '⚠️ REQUEST CHANGES';
295
+ }
296
+ }
297
+
298
+ const body = `## 🤖 AI Code Review - ${verdict}\n\n${review}\n\n---\n*Powered by GitHub Models (Codestral-2501) • 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\`*`;
299
+
300
+ // Post as a proper PR review (not an issue comment)
301
+ // This prevents duplication and integrates better with GitHub's review system
302
+ await github.rest.pulls.createReview({
303
+ owner: context.repo.owner,
304
+ repo: context.repo.repo,
305
+ pull_number: context.issue.number,
306
+ body: body,
307
+ event: event
308
+ });
309
+
310
+ - name: Check for critical issues
311
+ run: |
312
+ if grep -qi "REQUEST CHANGES\|CRITICAL\|SECURITY.*CONCERN" review.md; then
313
+ echo "::warning::AI reviewer found critical issues that need attention"
314
+ fi
315
+
316
+ # Alternative models (commented out - uncomment to switch):
317
+ #
318
+ # For Mistral Large:
319
+ # Replace "Codestral-2501" with "Mistral-large-2411"
320
+ # Model: https://github.com/marketplace/models/azureml-mistral/Mistral-large-2411
321
+ #
322
+ # For Mistral Small:
323
+ # Replace "Codestral-2501" with "Mistral-small"
324
+ # Model: https://github.com/marketplace/models/azureml-mistral/Mistral-small
325
+ #
326
+ # For Mistral Nemo:
327
+ # Replace "Codestral-2501" with "Mistral-Nemo"
328
+ # Model: https://github.com/marketplace/models/azureml-mistral/Mistral-Nemo
@@ -0,0 +1,78 @@
1
+ name: License Check
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+ schedule:
9
+ - cron: '0 8 * * 1' # Weekly on Monday at 8am UTC (after dependency updates)
10
+ workflow_dispatch: # Allow manual triggering
11
+
12
+ permissions:
13
+ contents: read
14
+ actions: read
15
+
16
+ jobs:
17
+ license-check:
18
+ name: Gem License Compliance
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v6
23
+
24
+ - name: Setup Ruby
25
+ uses: ruby/setup-ruby@v1
26
+ with:
27
+ ruby-version: 3.4.7
28
+ bundler-cache: true
29
+
30
+ - name: Make license check script executable
31
+ run: chmod +x scripts/license-check.rb
32
+
33
+ - name: Run License Check
34
+ run: ruby scripts/license-check.rb
35
+
36
+ - name: Upload license report as artifact
37
+ uses: actions/upload-artifact@v6
38
+ if: failure()
39
+ with:
40
+ name: license-check-report
41
+ path: |
42
+ Gemfile.lock
43
+ config/license_overrides.yml
44
+ retention-days: 30
45
+ if-no-files-found: ignore
46
+
47
+ license-audit:
48
+ name: License Audit (Monthly)
49
+ runs-on: ubuntu-latest
50
+ if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
51
+ steps:
52
+ - name: Checkout
53
+ uses: actions/checkout@v6
54
+
55
+ - name: Setup Ruby
56
+ uses: ruby/setup-ruby@v1
57
+ with:
58
+ ruby-version: 3.4.7
59
+ bundler-cache: true
60
+
61
+ - name: Install license_finder
62
+ run: gem install license_finder
63
+
64
+ - name: Generate detailed license report
65
+ run: |
66
+ echo "# License Audit Report" > license-audit-report.md
67
+ echo "Generated on: $(date)" >> license-audit-report.md
68
+ echo "" >> license-audit-report.md
69
+ echo "## All Gem Licenses" >> license-audit-report.md
70
+ echo "" >> license-audit-report.md
71
+ license_finder --format markdown >> license-audit-report.md
72
+
73
+ - name: Upload detailed license audit
74
+ uses: actions/upload-artifact@v6
75
+ with:
76
+ name: license-audit-report
77
+ path: license-audit-report.md
78
+ retention-days: 90
@@ -0,0 +1,79 @@
1
+ name: Lint
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+ pull_request:
7
+ branches: [ main, master ]
8
+
9
+ jobs:
10
+ rubocop:
11
+ name: RuboCop (Ruby Linting)
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout code
16
+ uses: actions/checkout@v6
17
+
18
+ - name: Set up Ruby
19
+ uses: ruby/setup-ruby@v1
20
+ with:
21
+ ruby-version: '3.4'
22
+ bundler-cache: true
23
+
24
+ - name: Run RuboCop
25
+ run: bundle exec rubocop --parallel
26
+
27
+ stylelint:
28
+ name: Stylelint (CSS Linting)
29
+ runs-on: ubuntu-latest
30
+
31
+ steps:
32
+ - name: Checkout code
33
+ uses: actions/checkout@v6
34
+
35
+ - name: Set up Node.js
36
+ uses: actions/setup-node@v6
37
+ with:
38
+ node-version: '20'
39
+ cache: 'yarn'
40
+
41
+ - name: Install dependencies
42
+ run: yarn install --frozen-lockfile
43
+
44
+ - name: Run Stylelint
45
+ run: yarn lint:css
46
+
47
+ brakeman:
48
+ name: Brakeman (Security Scan)
49
+ runs-on: ubuntu-latest
50
+
51
+ steps:
52
+ - name: Checkout code
53
+ uses: actions/checkout@v6
54
+
55
+ - name: Set up Ruby
56
+ uses: ruby/setup-ruby@v1
57
+ with:
58
+ ruby-version: '3.4'
59
+ bundler-cache: true
60
+
61
+ - name: Run Brakeman
62
+ run: bundle exec brakeman --quiet --no-pager
63
+
64
+ bundle-audit:
65
+ name: Bundle Audit (Dependency Security)
66
+ runs-on: ubuntu-latest
67
+
68
+ steps:
69
+ - name: Checkout code
70
+ uses: actions/checkout@v6
71
+
72
+ - name: Set up Ruby
73
+ uses: ruby/setup-ruby@v1
74
+ with:
75
+ ruby-version: '3.4'
76
+ bundler-cache: true
77
+
78
+ - name: Run Bundle Audit
79
+ run: bundle exec bundle-audit check --update
@@ -0,0 +1,131 @@
1
+ name: Security Scanning
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, develop ]
6
+ pull_request:
7
+ branches: [ main ]
8
+ schedule:
9
+ - cron: '0 6 * * 1' # Weekly on Monday at 6am UTC
10
+ workflow_dispatch: # Allow manual triggering
11
+
12
+ permissions:
13
+ contents: read
14
+ security-events: write
15
+ actions: read
16
+
17
+ jobs:
18
+ trufflehog:
19
+ name: TruffleHog Secret Scan
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v6
24
+ with:
25
+ fetch-depth: 0
26
+
27
+ - name: TruffleHog OSS
28
+ uses: trufflesecurity/trufflehog@main
29
+ with:
30
+ path: ./
31
+ base: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || 'HEAD~1' }}
32
+ head: HEAD
33
+ extra_args: --debug --only-verified
34
+ continue-on-error: false
35
+
36
+ semgrep:
37
+ name: Semgrep Security Analysis
38
+ runs-on: ubuntu-latest
39
+ if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'pull_request'
40
+ container:
41
+ image: semgrep/semgrep
42
+ steps:
43
+ - name: Checkout
44
+ uses: actions/checkout@v6
45
+
46
+ - name: Run Semgrep
47
+ env:
48
+ SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
49
+ run: |
50
+ if [ -n "$SEMGREP_APP_TOKEN" ]; then
51
+ echo "Running semgrep ci with token"
52
+ semgrep ci --json --output=semgrep-results.json || true
53
+ else
54
+ echo "Running semgrep with auto config (no token)"
55
+ semgrep --config=auto \
56
+ --exclude="*.min.js" \
57
+ --exclude="node_modules" \
58
+ --exclude="vendor" \
59
+ --exclude="tmp" \
60
+ --exclude="log" \
61
+ --exclude="public/assets" \
62
+ --json --output=semgrep-results.json . || true
63
+ fi
64
+ continue-on-error: false
65
+
66
+ - name: Upload Semgrep Results
67
+ uses: actions/upload-artifact@v6
68
+ if: always()
69
+ with:
70
+ name: semgrep-results
71
+ path: semgrep-results.json
72
+ retention-days: 30
73
+
74
+ semgrep-manual:
75
+ name: Manual Semgrep Trigger
76
+ runs-on: ubuntu-latest
77
+ if: github.event_name == 'workflow_dispatch'
78
+ container:
79
+ image: semgrep/semgrep
80
+ steps:
81
+ - name: Checkout
82
+ uses: actions/checkout@v6
83
+
84
+ - name: Run Semgrep (Manual)
85
+ env:
86
+ SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
87
+ run: semgrep ci
88
+ continue-on-error: false
89
+
90
+ dependency-check:
91
+ name: Dependency Vulnerability Scan
92
+ runs-on: ubuntu-latest
93
+ steps:
94
+ - name: Checkout
95
+ uses: actions/checkout@v6
96
+
97
+ - name: Setup Ruby
98
+ uses: ruby/setup-ruby@v1
99
+ with:
100
+ ruby-version: 3.4.7
101
+ bundler-cache: true
102
+
103
+ - name: Bundle Audit
104
+ run: |
105
+ gem install bundler-audit > /dev/null 2>&1
106
+ bundle-audit check --update
107
+
108
+ docker-security:
109
+ name: Docker Security Scan
110
+ runs-on: ubuntu-latest
111
+ steps:
112
+ - name: Checkout
113
+ uses: actions/checkout@v6
114
+
115
+ - name: Run Trivy config scanner on Dockerfile
116
+ uses: aquasecurity/trivy-action@master
117
+ with:
118
+ scan-type: 'config'
119
+ scan-ref: './Dockerfile'
120
+ format: 'sarif'
121
+ output: 'trivy-results.sarif'
122
+ continue-on-error: true
123
+
124
+ - name: Upload Trivy scan results as artifacts
125
+ uses: actions/upload-artifact@v6
126
+ if: always()
127
+ with:
128
+ name: trivy-security-results
129
+ path: trivy-results.sarif
130
+ retention-days: 30
131
+ if-no-files-found: ignore
@@ -0,0 +1,26 @@
1
+ on:
2
+ workflow_dispatch: {}
3
+ pull_request: {}
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ paths:
9
+ - .github/workflows/semgrep.yml
10
+ schedule:
11
+ # random HH:MM to avoid a load spike on GitHub Actions at 00:00
12
+ - cron: 1 14 * * *
13
+ name: Semgrep
14
+ jobs:
15
+ semgrep:
16
+ name: semgrep/ci
17
+ runs-on: ubuntu-latest
18
+ permissions:
19
+ contents: read
20
+ env:
21
+ SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
22
+ container:
23
+ image: semgrep/semgrep
24
+ steps:
25
+ - uses: actions/checkout@v6
26
+ - run: semgrep ci
@@ -0,0 +1,44 @@
1
+ name: Test with Coverage
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ ruby-version: [3.4.7]
11
+
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+
15
+ - name: Set up Ruby
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby-version }}
19
+ bundler-cache: true
20
+
21
+ - name: Install dependencies
22
+ run: |
23
+ gem install bundler
24
+ bundle install --jobs 4 --retry 3
25
+
26
+ - name: Run tests
27
+ run: |
28
+ bundle exec rake test
29
+
30
+ - name: Check test coverage
31
+ run: |
32
+ bundle exec rake coverage
33
+ # Verify coverage is at least 80%
34
+ if [ ! -f coverage/.last_run.json ]; then
35
+ echo "Coverage report not found"
36
+ exit 1
37
+ fi
38
+ coverage=$(grep -o '"covered_percent":[0-9.]*' coverage/.last_run.json | cut -d':' -f2)
39
+ if (( $(echo "$coverage < 80.0" | bc -l) )); then
40
+ echo "Coverage is below 80%: $coverage%"
41
+ exit 1
42
+ else
43
+ echo "Coverage is at least 80%: $coverage%"
44
+ fi