@ripla/godd-mcp 1.0.4-canary.8 → 1.0.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 (348) hide show
  1. package/README.md +89 -94
  2. package/dist/godd.cjs +349 -233
  3. package/dist/godd.js +2193 -838
  4. package/dist/index.js +152 -122
  5. package/{notes-api → notes/api}/.env.example +6 -1
  6. package/{notes-api → notes/api}/README.md +6 -5
  7. package/{notes-api → notes/api}/app/main.py +29 -2
  8. package/{notes-api → notes/api}/app/routers/auth.py +6 -1
  9. package/{notes-api → notes/api}/app/routers/issues.py +157 -1
  10. package/{notes-api → notes/api}/app/routers/pr.py +2 -2
  11. package/{notes-api → notes/api}/app/services/github_app_token.py +32 -1
  12. package/{notes-api → notes/api}/app/services/github_issues.py +101 -11
  13. package/{notes-api → notes/api}/app/services/github_pr.py +6 -1
  14. package/{notes-api → notes/api}/tests/test_github_app_token.py +49 -1
  15. package/{notes-api → notes/api}/tests/test_github_service.py +42 -0
  16. package/notes/api/tests/test_health.py +52 -0
  17. package/{notes-api → notes/api}/tests/test_issues.py +216 -0
  18. package/{notes-api → notes/api}/tests/test_pr.py +24 -0
  19. package/{notes-api → notes/api}/tests/test_pr_chain.py +19 -0
  20. package/{notes-api → notes/api}/uv.lock +178 -2
  21. package/notes/app/DESIGN.md +273 -0
  22. package/{notes-app → notes/app}/README.md +4 -1
  23. package/notes/app/docker/Dockerfile.e2e +19 -0
  24. package/notes/app/docker/Dockerfile.prod +15 -0
  25. package/notes/app/docker/Dockerfile.test +17 -0
  26. package/notes/app/e2e/file-tree.spec.ts +26 -0
  27. package/notes/app/e2e/file-view.spec.ts +25 -0
  28. package/notes/app/e2e/fixtures.ts +28 -0
  29. package/{notes-app → notes/app}/e2e/global-setup.ts +5 -7
  30. package/notes/app/e2e/login.spec.ts +25 -0
  31. package/notes/app/e2e/pages/login-page.ts +29 -0
  32. package/notes/app/e2e/pages/main-page.ts +29 -0
  33. package/notes/app/e2e/panel-resize-handle.spec.ts +213 -0
  34. package/{notes-app → notes/app}/eslint.config.js +9 -0
  35. package/{notes-app → notes/app}/package.json +0 -3
  36. package/{notes-app → notes/app}/pnpm-workspace.yaml +1 -0
  37. package/notes/app/src/components/CommentPanel.test.tsx +194 -0
  38. package/{notes-app → notes/app}/src/components/CommentPanel.tsx +119 -10
  39. package/notes/app/src/components/CommentSelectionPopover.tsx +26 -0
  40. package/notes/app/src/components/EditToolbar.test.tsx +102 -0
  41. package/{notes-app → notes/app}/src/components/EditToolbar.tsx +5 -54
  42. package/notes/app/src/components/FileInfoPanel.branch-tab.test.tsx +250 -0
  43. package/notes/app/src/components/FileInfoPanel.history-tab.test.tsx +146 -0
  44. package/notes/app/src/components/FileInfoPanel.test.tsx +101 -0
  45. package/{notes-app → notes/app}/src/components/FileInfoPanel.tsx +135 -20
  46. package/notes/app/src/components/IssueCommentThread.test.tsx +93 -0
  47. package/notes/app/src/components/IssueCommentThread.tsx +156 -0
  48. package/notes/app/src/components/IssueDetailView.test.tsx +1118 -0
  49. package/notes/app/src/components/IssueDetailView.tsx +476 -0
  50. package/notes/app/src/components/IssueList.test.tsx +545 -0
  51. package/notes/app/src/components/IssueList.tsx +374 -0
  52. package/{notes-app → notes/app}/src/components/LabelSelector.test.tsx +94 -8
  53. package/notes/app/src/components/LabelSelector.tsx +452 -0
  54. package/{notes-app → notes/app}/src/components/MarkdownEditor.tsx +140 -2
  55. package/notes/app/src/components/PanelResizeHandle.test.tsx +481 -0
  56. package/notes/app/src/components/PanelResizeHandle.tsx +96 -0
  57. package/{notes-app → notes/app}/src/hooks/useEditSession.test.ts +171 -23
  58. package/{notes-app → notes/app}/src/hooks/useEditSession.ts +49 -62
  59. package/notes/app/src/hooks/useMdUpLayout.test.ts +10 -0
  60. package/notes/app/src/hooks/useMdUpLayout.ts +27 -0
  61. package/notes/app/src/hooks/usePeriodicSync.test.ts +244 -0
  62. package/notes/app/src/hooks/usePeriodicSync.ts +83 -0
  63. package/notes/app/src/hooks/useResizablePanelWidth.test.ts +251 -0
  64. package/notes/app/src/hooks/useResizablePanelWidth.ts +167 -0
  65. package/{notes-app → notes/app}/src/lib/api.test.ts +81 -1
  66. package/{notes-app → notes/app}/src/lib/api.ts +98 -4
  67. package/notes/app/src/lib/commentAnchor.test.ts +22 -0
  68. package/notes/app/src/lib/commentAnchor.ts +28 -0
  69. package/notes/app/src/lib/commentHighlight.test.ts +110 -0
  70. package/notes/app/src/lib/commentHighlight.ts +44 -0
  71. package/notes/app/src/lib/github-sync.test.ts +126 -0
  72. package/notes/app/src/lib/github-sync.ts +89 -0
  73. package/notes/app/src/lib/issue-list-utils.test.ts +275 -0
  74. package/notes/app/src/lib/issue-list-utils.ts +135 -0
  75. package/notes/app/src/lib/issue-mobile-layout.test.ts +17 -0
  76. package/notes/app/src/lib/issue-mobile-layout.ts +20 -0
  77. package/notes/app/src/lib/issue-url-sync.test.ts +73 -0
  78. package/notes/app/src/lib/issue-url-sync.ts +43 -0
  79. package/notes/app/src/lib/main-page-default-view.test.ts +37 -0
  80. package/notes/app/src/lib/main-page-default-view.ts +15 -0
  81. package/notes/app/src/lib/resizable-panel.test.ts +199 -0
  82. package/notes/app/src/lib/resizable-panel.ts +136 -0
  83. package/notes/app/src/pages/LoginPage.test.tsx +17 -0
  84. package/notes/app/src/pages/MainPage.branch-modal.test.tsx +226 -0
  85. package/notes/app/src/pages/MainPage.issue-sync.test.tsx +145 -0
  86. package/notes/app/src/pages/MainPage.sidebar.test.tsx +361 -0
  87. package/{notes-app → notes/app}/src/pages/MainPage.tsx +406 -61
  88. package/{notes-app → notes/app}/src/pages/SettingsPage.test.tsx +32 -23
  89. package/{notes-app → notes/app}/src/pages/UserManagementPage.test.tsx +8 -18
  90. package/notes/app/src/test/mocks.ts +156 -0
  91. package/notes/app/src/test/render.tsx +27 -0
  92. package/package.json +3 -5
  93. package/notes-api/tests/test_health.py +0 -11
  94. package/notes-app/docker/Dockerfile.e2e +0 -14
  95. package/notes-app/docker/Dockerfile.prod +0 -12
  96. package/notes-app/docker/Dockerfile.test +0 -12
  97. package/notes-app/e2e/file-tree.spec.ts +0 -29
  98. package/notes-app/e2e/file-view.spec.ts +0 -24
  99. package/notes-app/e2e/login.spec.ts +0 -32
  100. package/notes-app/src/components/IssueDetailView.test.tsx +0 -201
  101. package/notes-app/src/components/IssueDetailView.tsx +0 -318
  102. package/notes-app/src/components/IssueList.test.tsx +0 -93
  103. package/notes-app/src/components/IssueList.tsx +0 -169
  104. package/notes-app/src/components/LabelSelector.tsx +0 -276
  105. package/notes-app/src/pages/LoginPage.test.tsx +0 -29
  106. package/stacks/django-vue.yaml +0 -33
  107. package/stacks/fastapi-react.yaml +0 -160
  108. package/stacks/flutter-firebase.yaml +0 -58
  109. package/stacks/nextjs-prisma.yaml +0 -33
  110. package/stacks/schema.json +0 -144
  111. package/templates/agents/architect.hbs +0 -183
  112. package/templates/agents/auto-documenter.hbs +0 -36
  113. package/templates/agents/backend-developer.hbs +0 -175
  114. package/templates/agents/code-reviewer.hbs +0 -29
  115. package/templates/agents/database-developer.hbs +0 -52
  116. package/templates/agents/debug-specialist.hbs +0 -39
  117. package/templates/agents/environment-setup.hbs +0 -41
  118. package/templates/agents/frontend-developer.hbs +0 -45
  119. package/templates/agents/integration-qa.hbs +0 -26
  120. package/templates/agents/performance-qa.hbs +0 -24
  121. package/templates/agents/pr-writer.hbs +0 -53
  122. package/templates/agents/quality-lead.hbs +0 -26
  123. package/templates/agents/requirements-analyst.hbs +0 -26
  124. package/templates/agents/security-analyst.hbs +0 -21
  125. package/templates/agents/security-reviewer.hbs +0 -20
  126. package/templates/agents/ssot-updater.hbs +0 -45
  127. package/templates/agents/technical-writer.hbs +0 -28
  128. package/templates/agents/unit-test-engineer.hbs +0 -19
  129. package/templates/agents/user-clone.hbs +0 -43
  130. package/templates/github-actions/aws/deploy-notes.yml.hbs +0 -117
  131. package/templates/github-actions/azure/deploy-notes.yml.hbs +0 -78
  132. package/templates/github-actions/gcp/deploy-notes.yml.hbs +0 -76
  133. package/templates/github-actions/vercel-railway/deploy-notes.yml.hbs +0 -49
  134. package/templates/mindsets/agent-stdio.hbs +0 -51
  135. package/templates/mindsets/dart.hbs +0 -39
  136. package/templates/mindsets/ddd-clean-architecture.hbs +0 -52
  137. package/templates/mindsets/electron.hbs +0 -41
  138. package/templates/mindsets/fastapi.hbs +0 -40
  139. package/templates/mindsets/flutter.hbs +0 -56
  140. package/templates/mindsets/kotlin.hbs +0 -46
  141. package/templates/mindsets/postgresql.hbs +0 -39
  142. package/templates/mindsets/python.hbs +0 -39
  143. package/templates/mindsets/react.hbs +0 -51
  144. package/templates/mindsets/swift.hbs +0 -46
  145. package/templates/mindsets/terraform.hbs +0 -53
  146. package/templates/mindsets/typescript.hbs +0 -34
  147. package/templates/mindsets/vite.hbs +0 -37
  148. package/templates/notes-compose.yml +0 -82
  149. package/templates/partials/cogito-geo.hbs +0 -84
  150. package/templates/partials/cogito-godd.hbs +0 -22
  151. package/templates/partials/cogito-grammar.hbs +0 -105
  152. package/templates/partials/cogito-particle.hbs +0 -76
  153. package/templates/partials/cogito-root-ai.hbs +0 -79
  154. package/templates/partials/cogito-root-arch.hbs +0 -75
  155. package/templates/partials/cogito-root-core.hbs +0 -75
  156. package/templates/partials/cogito-root-daily.hbs +0 -58
  157. package/templates/partials/cogito-root-dev.hbs +0 -71
  158. package/templates/partials/cogito-root-devops.hbs +0 -71
  159. package/templates/partials/cogito-root-framework.hbs +0 -80
  160. package/templates/partials/cogito-root-lang.hbs +0 -74
  161. package/templates/partials/godd-core.hbs +0 -50
  162. package/templates/partials/project-context.hbs +0 -52
  163. package/templates/partials/ssot-header.hbs +0 -40
  164. package/templates/partials/ssot-rules.hbs +0 -30
  165. package/templates/prompts/adr.hbs +0 -39
  166. package/templates/prompts/agent-dev-workflow.hbs +0 -34
  167. package/templates/prompts/analyze-report.hbs +0 -36
  168. package/templates/prompts/auto-documentation.hbs +0 -37
  169. package/templates/prompts/check.hbs +0 -52
  170. package/templates/prompts/commit.hbs +0 -96
  171. package/templates/prompts/dev.hbs +0 -127
  172. package/templates/prompts/docs-init.hbs +0 -101
  173. package/templates/prompts/docs-update.hbs +0 -51
  174. package/templates/prompts/git-workflow.hbs +0 -125
  175. package/templates/prompts/impact-analysis.hbs +0 -43
  176. package/templates/prompts/install.hbs +0 -40
  177. package/templates/prompts/new-branch.hbs +0 -61
  178. package/templates/prompts/notes-deploy.hbs +0 -138
  179. package/templates/prompts/pr-analyze.hbs +0 -44
  180. package/templates/prompts/pr-create.hbs +0 -125
  181. package/templates/prompts/push-execute.hbs +0 -83
  182. package/templates/prompts/push.hbs +0 -24
  183. package/templates/prompts/release-notes.hbs +0 -38
  184. package/templates/prompts/requirements-to-business-flow.hbs +0 -29
  185. package/templates/prompts/requirements-to-tickets.hbs +0 -26
  186. package/templates/prompts/review.hbs +0 -126
  187. package/templates/prompts/setup.hbs +0 -247
  188. package/templates/prompts/spec.hbs +0 -35
  189. package/templates/prompts/specification-to-tickets.hbs +0 -24
  190. package/templates/prompts/submit.hbs +0 -143
  191. package/templates/prompts/sync.hbs +0 -63
  192. package/templates/prompts/test-plan.hbs +0 -56
  193. package/templates/scripts/aws/terraform-backend-setup.sh.hbs +0 -149
  194. package/templates/terraform/aws/alb.tf.hbs +0 -58
  195. package/templates/terraform/aws/backend.tf.hbs +0 -10
  196. package/templates/terraform/aws/cloudfront.tf.hbs +0 -96
  197. package/templates/terraform/aws/ecr.tf.hbs +0 -30
  198. package/templates/terraform/aws/ecs.tf.hbs +0 -142
  199. package/templates/terraform/aws/iam.tf.hbs +0 -88
  200. package/templates/terraform/aws/local.tf.hbs +0 -34
  201. package/templates/terraform/aws/output.tf.hbs +0 -49
  202. package/templates/terraform/aws/provider.tf.hbs +0 -26
  203. package/templates/terraform/aws/rds.tf.hbs +0 -38
  204. package/templates/terraform/aws/s3.tf.hbs +0 -34
  205. package/templates/terraform/aws/secrets.tf.hbs +0 -53
  206. package/templates/terraform/aws/security_groups.tf.hbs +0 -81
  207. package/templates/terraform/aws/vpc.tf.hbs +0 -63
  208. package/templates/terraform/azure/backend.tf.hbs +0 -8
  209. package/templates/terraform/azure/iam.tf.hbs +0 -36
  210. package/templates/terraform/azure/main.tf.hbs +0 -203
  211. package/templates/terraform/azure/output.tf.hbs +0 -19
  212. package/templates/terraform/azure/provider.tf.hbs +0 -19
  213. package/templates/terraform/gcp/backend.tf.hbs +0 -6
  214. package/templates/terraform/gcp/iam.tf.hbs +0 -47
  215. package/templates/terraform/gcp/main.tf.hbs +0 -175
  216. package/templates/terraform/gcp/output.tf.hbs +0 -15
  217. package/templates/terraform/gcp/provider.tf.hbs +0 -19
  218. package/templates/terraform/vercel-railway/main.tf.hbs +0 -64
  219. package/templates/terraform/vercel-railway/output.tf.hbs +0 -11
  220. /package/{notes-api → notes/api}/.dockerignore +0 -0
  221. /package/{notes-api → notes/api}/alembic/README +0 -0
  222. /package/{notes-api → notes/api}/alembic/env.py +0 -0
  223. /package/{notes-api → notes/api}/alembic/script.py.mako +0 -0
  224. /package/{notes-api → notes/api}/alembic/versions/001_initial_notes_tables.py +0 -0
  225. /package/{notes-api → notes/api}/alembic/versions/002_add_performance_indexes.py +0 -0
  226. /package/{notes-api → notes/api}/alembic/versions/003_add_yjs_documents.py +0 -0
  227. /package/{notes-api → notes/api}/alembic/versions/004_add_comments.py +0 -0
  228. /package/{notes-api → notes/api}/alembic/versions/005_add_github_username.py +0 -0
  229. /package/{notes-api → notes/api}/alembic/versions/006_add_check_constraints.py +0 -0
  230. /package/{notes-api → notes/api}/alembic.ini +0 -0
  231. /package/{notes-api → notes/api}/app/__init__.py +0 -0
  232. /package/{notes-api → notes/api}/app/config.py +0 -0
  233. /package/{notes-api → notes/api}/app/database.py +0 -0
  234. /package/{notes-api → notes/api}/app/http_client.py +0 -0
  235. /package/{notes-api → notes/api}/app/models/__init__.py +0 -0
  236. /package/{notes-api → notes/api}/app/models/audit.py +0 -0
  237. /package/{notes-api → notes/api}/app/models/base.py +0 -0
  238. /package/{notes-api → notes/api}/app/models/collab.py +0 -0
  239. /package/{notes-api → notes/api}/app/models/comment.py +0 -0
  240. /package/{notes-api → notes/api}/app/models/session.py +0 -0
  241. /package/{notes-api → notes/api}/app/models/setting.py +0 -0
  242. /package/{notes-api → notes/api}/app/models/user.py +0 -0
  243. /package/{notes-api → notes/api}/app/path_validation.py +0 -0
  244. /package/{notes-api → notes/api}/app/routers/__init__.py +0 -0
  245. /package/{notes-api → notes/api}/app/routers/collab.py +0 -0
  246. /package/{notes-api → notes/api}/app/routers/comments.py +0 -0
  247. /package/{notes-api → notes/api}/app/routers/files.py +0 -0
  248. /package/{notes-api → notes/api}/app/routers/settings.py +0 -0
  249. /package/{notes-api → notes/api}/app/routers/tree.py +0 -0
  250. /package/{notes-api → notes/api}/app/routers/upload.py +0 -0
  251. /package/{notes-api → notes/api}/app/routers/users.py +0 -0
  252. /package/{notes-api → notes/api}/app/schemas/__init__.py +0 -0
  253. /package/{notes-api → notes/api}/app/schemas/auth.py +0 -0
  254. /package/{notes-api → notes/api}/app/schemas/user.py +0 -0
  255. /package/{notes-api → notes/api}/app/security.py +0 -0
  256. /package/{notes-api → notes/api}/app/services/__init__.py +0 -0
  257. /package/{notes-api → notes/api}/app/services/audit.py +0 -0
  258. /package/{notes-api → notes/api}/app/services/business_date.py +0 -0
  259. /package/{notes-api → notes/api}/app/services/collab.py +0 -0
  260. /package/{notes-api → notes/api}/app/services/github.py +0 -0
  261. /package/{notes-api → notes/api}/app/services/pr_chain.py +0 -0
  262. /package/{notes-api → notes/api}/app/startup.py +0 -0
  263. /package/{notes-api → notes/api}/docker/Dockerfile +0 -0
  264. /package/{notes-api → notes/api}/docker/Dockerfile.test +0 -0
  265. /package/{notes-api → notes/api}/pyproject.toml +0 -0
  266. /package/{notes-api → notes/api}/tests/conftest.py +0 -0
  267. /package/{notes-api → notes/api}/tests/test_audit.py +0 -0
  268. /package/{notes-api → notes/api}/tests/test_auth.py +0 -0
  269. /package/{notes-api → notes/api}/tests/test_business_date.py +0 -0
  270. /package/{notes-api → notes/api}/tests/test_collab.py +0 -0
  271. /package/{notes-api → notes/api}/tests/test_comments.py +0 -0
  272. /package/{notes-api → notes/api}/tests/test_config_ssl.py +0 -0
  273. /package/{notes-api → notes/api}/tests/test_config_warnings.py +0 -0
  274. /package/{notes-api → notes/api}/tests/test_files.py +0 -0
  275. /package/{notes-api → notes/api}/tests/test_path_validation.py +0 -0
  276. /package/{notes-api → notes/api}/tests/test_security.py +0 -0
  277. /package/{notes-api → notes/api}/tests/test_settings.py +0 -0
  278. /package/{notes-api → notes/api}/tests/test_tree.py +0 -0
  279. /package/{notes-api → notes/api}/tests/test_upload.py +0 -0
  280. /package/{notes-api → notes/api}/tests/test_users.py +0 -0
  281. /package/{notes-app → notes/app}/.dockerignore +0 -0
  282. /package/{notes-app → notes/app}/index.html +0 -0
  283. /package/{notes-app → notes/app}/nginx.conf +0 -0
  284. /package/{notes-app → notes/app}/package-lock.json +0 -0
  285. /package/{notes-app → notes/app}/playwright.config.ts +0 -0
  286. /package/{notes-app → notes/app}/pnpm-lock.yaml +0 -0
  287. /package/{notes-app → notes/app}/src/App.tsx +0 -0
  288. /package/{notes-app → notes/app}/src/components/BranchSelectorModal.tsx +0 -0
  289. /package/{notes-app → notes/app}/src/components/CodeBlockView.test.tsx +0 -0
  290. /package/{notes-app → notes/app}/src/components/CodeBlockView.tsx +0 -0
  291. /package/{notes-app → notes/app}/src/components/CollabStatus.tsx +0 -0
  292. /package/{notes-app → notes/app}/src/components/DiffPreview.tsx +0 -0
  293. /package/{notes-app → notes/app}/src/components/DrawioEditor.tsx +0 -0
  294. /package/{notes-app → notes/app}/src/components/DrawioViewer.tsx +0 -0
  295. /package/{notes-app → notes/app}/src/components/ErrorBoundary.test.tsx +0 -0
  296. /package/{notes-app → notes/app}/src/components/ErrorBoundary.tsx +0 -0
  297. /package/{notes-app → notes/app}/src/components/FileContentView.tsx +0 -0
  298. /package/{notes-app → notes/app}/src/components/FileContextMenu.test.tsx +0 -0
  299. /package/{notes-app → notes/app}/src/components/FileContextMenu.tsx +0 -0
  300. /package/{notes-app → notes/app}/src/components/FileDialogs.test.tsx +0 -0
  301. /package/{notes-app → notes/app}/src/components/FileDialogs.tsx +0 -0
  302. /package/{notes-app → notes/app}/src/components/SaveStatusIndicator.test.tsx +0 -0
  303. /package/{notes-app → notes/app}/src/components/SaveStatusIndicator.tsx +0 -0
  304. /package/{notes-app → notes/app}/src/components/Skeleton.tsx +0 -0
  305. /package/{notes-app → notes/app}/src/components/SpreadsheetEditor.test.tsx +0 -0
  306. /package/{notes-app → notes/app}/src/components/SpreadsheetEditor.tsx +0 -0
  307. /package/{notes-app → notes/app}/src/components/TextEditor.tsx +0 -0
  308. /package/{notes-app → notes/app}/src/components/ToastContainer.tsx +0 -0
  309. /package/{notes-app → notes/app}/src/components/TreeNode.tsx +0 -0
  310. /package/{notes-app → notes/app}/src/contexts/AuthContext.test.tsx +0 -0
  311. /package/{notes-app → notes/app}/src/contexts/AuthContext.tsx +0 -0
  312. /package/{notes-app → notes/app}/src/contexts/ToastContext.test.ts +0 -0
  313. /package/{notes-app → notes/app}/src/contexts/ToastContext.tsx +0 -0
  314. /package/{notes-app → notes/app}/src/hooks/useAutoSave.test.ts +0 -0
  315. /package/{notes-app → notes/app}/src/hooks/useAutoSave.ts +0 -0
  316. /package/{notes-app → notes/app}/src/hooks/useCollaboration.ts +0 -0
  317. /package/{notes-app → notes/app}/src/hooks/useComments.ts +0 -0
  318. /package/{notes-app → notes/app}/src/hooks/useYText.ts +0 -0
  319. /package/{notes-app → notes/app}/src/lib/branch-diff.test.ts +0 -0
  320. /package/{notes-app → notes/app}/src/lib/branch-diff.ts +0 -0
  321. /package/{notes-app → notes/app}/src/lib/created-file.test.ts +0 -0
  322. /package/{notes-app → notes/app}/src/lib/created-file.ts +0 -0
  323. /package/{notes-app → notes/app}/src/lib/csv-utils.test.ts +0 -0
  324. /package/{notes-app → notes/app}/src/lib/csv-utils.ts +0 -0
  325. /package/{notes-app → notes/app}/src/lib/deepLinkPathToSearch.test.ts +0 -0
  326. /package/{notes-app → notes/app}/src/lib/deepLinkPathToSearch.ts +0 -0
  327. /package/{notes-app → notes/app}/src/lib/draft-storage.test.ts +0 -0
  328. /package/{notes-app → notes/app}/src/lib/draft-storage.ts +0 -0
  329. /package/{notes-app → notes/app}/src/lib/mermaid-render.ts +0 -0
  330. /package/{notes-app → notes/app}/src/lib/postLoginRedirect.test.ts +0 -0
  331. /package/{notes-app → notes/app}/src/lib/postLoginRedirect.ts +0 -0
  332. /package/{notes-app → notes/app}/src/lib/tree-utils.ts +0 -0
  333. /package/{notes-app → notes/app}/src/lib/user-colors.ts +0 -0
  334. /package/{notes-app → notes/app}/src/lib/xlsx-utils.test.ts +0 -0
  335. /package/{notes-app → notes/app}/src/lib/xlsx-utils.ts +0 -0
  336. /package/{notes-app → notes/app}/src/main.tsx +0 -0
  337. /package/{notes-app → notes/app}/src/pages/LoginPage.tsx +0 -0
  338. /package/{notes-app → notes/app}/src/pages/MainPage.insertChildEntry.test.ts +0 -0
  339. /package/{notes-app → notes/app}/src/pages/SettingsPage.tsx +0 -0
  340. /package/{notes-app → notes/app}/src/pages/UserManagementPage.tsx +0 -0
  341. /package/{notes-app → notes/app}/src/styles/globals.css +0 -0
  342. /package/{notes-app → notes/app}/src/types.ts +0 -0
  343. /package/{notes-app → notes/app}/src/vite-env.d.ts +0 -0
  344. /package/{notes-app → notes/app}/tsconfig.json +0 -0
  345. /package/{notes-app → notes/app}/tsconfig.node.json +0 -0
  346. /package/{notes-app → notes/app}/vite.config.js +0 -0
  347. /package/{notes-app → notes/app}/vite.config.ts +0 -0
  348. /package/{notes-app → notes/app}/vitest.config.ts +0 -0
@@ -1,6 +1,11 @@
1
- # GitHub API token (omitted for mock mode)
1
+ # GitHub API token (PAT — omitted for mock mode; superseded by GitHub App below)
2
2
  # GITHUB_TOKEN=
3
3
 
4
+ # GitHub App credentials (recommended over PAT for bot authorship of PRs/Issues)
5
+ # GITHUB_APP_ID=
6
+ # GITHUB_APP_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----
7
+ # GITHUB_APP_INSTALLATION_ID=
8
+
4
9
  # Repository settings
5
10
  GITHUB_OWNER=ripla
6
11
  GITHUB_REPO=GoDD
@@ -39,10 +39,10 @@ uv run python -m app.main
39
39
 
40
40
  | 変数 | 必須 | デフォルト | 説明 |
41
41
  |------|------|-----------|------|
42
- | GITHUB_TOKEN | - | (未設定=Mock) | GitHub API PATGitHub App 未設定時のフォールバック) |
43
- | GITHUB_APP_ID | - | - | GitHub App ID。`GITHUB_APP_PRIVATE_KEY` / `GITHUB_APP_INSTALLATION_ID` と揃う場合は App token を優先 |
44
- | GITHUB_APP_PRIVATE_KEY | - | - | GitHub App private key PEM |
45
- | GITHUB_APP_INSTALLATION_ID | - | - | GitHub App installation ID |
42
+ | GITHUB_TOKEN | - | (未設定=Mock) | GitHub API PAT(任意。GitHub App 未設定時の write 操作フォールバック) |
43
+ | GITHUB_APP_ID | **Yes** (write) | - | GitHub App ID。PR / Issue 作成時の Bot author に必須 |
44
+ | GITHUB_APP_PRIVATE_KEY | **Yes** (write) | - | GitHub App private key PEM |
45
+ | GITHUB_APP_INSTALLATION_ID | **Yes** (write) | - | GitHub App installation ID |
46
46
  | GITHUB_OWNER | - | ripla-inc | リポジトリオーナー |
47
47
  | GITHUB_REPO | - | GoDD | リポジトリ名 |
48
48
  | GITHUB_BRANCH | - | main | ベースブランチ |
@@ -54,7 +54,8 @@ uv run python -m app.main
54
54
 
55
55
  ## API
56
56
 
57
- - `GET /api/health` - ヘルスチェック
57
+ - `GET /api/health` - ヘルスチェック(`github_app_configured`, `github_write_author`)
58
+ - `GET /api/health/github-app` - GitHub App installation token refresh 検証(運用 #829)
58
59
  - `POST /api/auth/login` - ログイン
59
60
  - `GET /api/tree` - ファイルツリー
60
61
  - `GET /api/files/{path}` - ファイル内容取得
@@ -3,7 +3,7 @@
3
3
  from collections.abc import AsyncIterator
4
4
  from contextlib import asynccontextmanager
5
5
 
6
- from fastapi import FastAPI, Request, Response
6
+ from fastapi import FastAPI, HTTPException, Request, Response
7
7
  from fastapi.middleware.cors import CORSMiddleware
8
8
  from starlette.middleware.base import BaseHTTPMiddleware
9
9
 
@@ -11,6 +11,11 @@ from app.config import settings
11
11
  from app.http_client import close_github_client
12
12
  from app.routers import auth, collab, comments, files, issues, pr, tree, upload, users
13
13
  from app.routers import settings as settings_router
14
+ from app.services.github_app_token import (
15
+ get_installation_token,
16
+ github_write_author_mode,
17
+ is_app_configured,
18
+ )
14
19
  from app.startup import ensure_tables_and_seed
15
20
 
16
21
 
@@ -70,7 +75,29 @@ app.include_router(comments.router)
70
75
  @app.get("/api/health")
71
76
  async def health_check():
72
77
  """Health check endpoint."""
73
- return {"status": "ok"}
78
+ return {
79
+ "status": "ok",
80
+ "github_app_configured": is_app_configured(),
81
+ "github_write_author": github_write_author_mode(),
82
+ }
83
+
84
+
85
+ @app.get("/api/health/github-app")
86
+ async def github_app_health_check():
87
+ """Verify GitHub App credentials by refreshing an installation token (#829 ops)."""
88
+ if not is_app_configured():
89
+ raise HTTPException(
90
+ status_code=503,
91
+ detail="GitHub App credentials are not fully configured",
92
+ )
93
+ try:
94
+ await get_installation_token()
95
+ except Exception as exc:
96
+ raise HTTPException(
97
+ status_code=503,
98
+ detail=f"GitHub App installation token refresh failed: {exc}",
99
+ ) from exc
100
+ return {"status": "ok", "github_write_author": "bot", "token_refresh": "success"}
74
101
 
75
102
 
76
103
  if __name__ == "__main__":
@@ -11,7 +11,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
11
11
  from app.config import settings
12
12
  from app.database import get_db
13
13
  from app.models import NotesSetting, NotesUser
14
- from app.schemas.auth import ChangePasswordRequest, LoginRequest, TokenResponse, UserInfo
14
+ from app.schemas.auth import (
15
+ ChangePasswordRequest,
16
+ LoginRequest,
17
+ TokenResponse,
18
+ UserInfo,
19
+ )
15
20
  from app.security import (
16
21
  auth_dependency,
17
22
  create_access_token,
@@ -13,11 +13,15 @@ from app.security import auth_dependency, require_role
13
13
  from app.services.github import get_github_config
14
14
  from app.services.github_issues import (
15
15
  create_issue,
16
+ create_issue_comment,
16
17
  create_label,
18
+ delete_label,
17
19
  get_issue,
20
+ list_issue_comments,
18
21
  list_issue_labels,
19
22
  list_issues,
20
23
  update_issue,
24
+ update_label,
21
25
  )
22
26
 
23
27
  logger = logging.getLogger(__name__)
@@ -29,6 +33,7 @@ MAX_BODY_LENGTH = 65_536
29
33
  MAX_LABEL_NAME_LENGTH = 50
30
34
  MAX_LABEL_DESCRIPTION_LENGTH = 200
31
35
  _HEX_COLOR_RE = r"^[0-9a-fA-F]{6}$"
36
+ _GITHUB_LOGIN_RE = r"^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$"
32
37
 
33
38
 
34
39
  class LabelCreateBody(BaseModel):
@@ -39,6 +44,14 @@ class LabelCreateBody(BaseModel):
39
44
  description: str = Field(default="", max_length=MAX_LABEL_DESCRIPTION_LENGTH)
40
45
 
41
46
 
47
+ class LabelUpdateBody(BaseModel):
48
+ """Label update request (at least one field required)."""
49
+
50
+ new_name: str | None = Field(None, min_length=1, max_length=MAX_LABEL_NAME_LENGTH)
51
+ color: str | None = Field(None, pattern=_HEX_COLOR_RE)
52
+ description: str | None = Field(None, max_length=MAX_LABEL_DESCRIPTION_LENGTH)
53
+
54
+
42
55
  class IssueCreateBody(BaseModel):
43
56
  """Issue creation request."""
44
57
 
@@ -56,10 +69,17 @@ class IssueUpdateBody(BaseModel):
56
69
  labels: list[str] | None = None
57
70
 
58
71
 
72
+ class IssueCommentCreateBody(BaseModel):
73
+ """Issue comment creation request."""
74
+
75
+ body: str = Field(..., min_length=1, max_length=MAX_BODY_LENGTH)
76
+
77
+
59
78
  @router.get("")
60
79
  async def list_issues_endpoint(
61
80
  state: str = Query("open", pattern=r"^(open|closed|all)$"),
62
81
  labels: str | None = Query(None),
82
+ assignee: str | None = Query(None, pattern=rf"^(none|{_GITHUB_LOGIN_RE[1:-1]})$"),
63
83
  page: int = Query(1, ge=1),
64
84
  per_page: int = Query(30, ge=1, le=100),
65
85
  _payload: dict = Depends(auth_dependency),
@@ -70,7 +90,12 @@ async def list_issues_endpoint(
70
90
  try:
71
91
  config = get_github_config()
72
92
  items = await list_issues(
73
- config, state=state, labels=labels, per_page=per_page, page=page,
93
+ config,
94
+ state=state,
95
+ labels=labels,
96
+ assignee=assignee,
97
+ per_page=per_page,
98
+ page=page,
74
99
  )
75
100
  return {"issues": items}
76
101
  except httpx.HTTPStatusError as e:
@@ -141,6 +166,75 @@ async def create_label_endpoint(
141
166
  raise HTTPException(status_code=500, detail="Internal server error") from None
142
167
 
143
168
 
169
+ @router.patch("/labels/{name}")
170
+ async def update_label_endpoint(
171
+ name: str,
172
+ body: LabelUpdateBody,
173
+ _payload: dict = Depends(auth_dependency),
174
+ __: dict = Depends(require_role("admin", "editor")),
175
+ ):
176
+ """Update an existing repository label."""
177
+ if not any(
178
+ field is not None for field in (body.new_name, body.color, body.description)
179
+ ):
180
+ raise HTTPException(status_code=422, detail="At least one field must be provided")
181
+ if settings.is_mock_mode():
182
+ raise HTTPException(status_code=400, detail="Mock mode: label update disabled")
183
+ try:
184
+ config = get_github_config()
185
+ return await update_label(
186
+ config,
187
+ name,
188
+ new_name=body.new_name,
189
+ color=body.color,
190
+ description=body.description,
191
+ )
192
+ except ValueError as e:
193
+ raise HTTPException(status_code=422, detail=str(e)) from None
194
+ except httpx.HTTPStatusError as e:
195
+ if e.response.status_code == 404:
196
+ raise HTTPException(status_code=404, detail="Label not found") from None
197
+ if e.response.status_code in (401, 403):
198
+ raise HTTPException(
199
+ status_code=e.response.status_code,
200
+ detail="GitHub token is invalid or expired",
201
+ ) from None
202
+ if e.response.status_code == 422:
203
+ raise HTTPException(
204
+ status_code=422, detail="Label name already exists or invalid data",
205
+ ) from None
206
+ raise HTTPException(status_code=502, detail="GitHub API error") from e
207
+ except Exception:
208
+ logger.exception("Failed to update label %s", name)
209
+ raise HTTPException(status_code=500, detail="Internal server error") from None
210
+
211
+
212
+ @router.delete("/labels/{name}", status_code=204)
213
+ async def delete_label_endpoint(
214
+ name: str,
215
+ _payload: dict = Depends(auth_dependency),
216
+ __: dict = Depends(require_role("admin", "editor")),
217
+ ):
218
+ """Delete a repository label."""
219
+ if settings.is_mock_mode():
220
+ raise HTTPException(status_code=400, detail="Mock mode: label deletion disabled")
221
+ try:
222
+ config = get_github_config()
223
+ await delete_label(config, name)
224
+ except httpx.HTTPStatusError as e:
225
+ if e.response.status_code == 404:
226
+ raise HTTPException(status_code=404, detail="Label not found") from None
227
+ if e.response.status_code in (401, 403):
228
+ raise HTTPException(
229
+ status_code=e.response.status_code,
230
+ detail="GitHub token is invalid or expired",
231
+ ) from None
232
+ raise HTTPException(status_code=502, detail="GitHub API error") from e
233
+ except Exception:
234
+ logger.exception("Failed to delete label %s", name)
235
+ raise HTTPException(status_code=500, detail="Internal server error") from None
236
+
237
+
144
238
  @router.get("/{number}")
145
239
  async def get_issue_endpoint(
146
240
  number: int,
@@ -233,3 +327,65 @@ async def update_issue_endpoint(
233
327
  except Exception:
234
328
  logger.exception("Failed to update issue #%d", number)
235
329
  raise HTTPException(status_code=500, detail="Internal server error") from None
330
+
331
+
332
+ @router.get("/{number}/comments")
333
+ async def list_issue_comments_endpoint(
334
+ number: int,
335
+ _payload: dict = Depends(auth_dependency),
336
+ ):
337
+ """List comments on an issue (chronological)."""
338
+ if settings.is_mock_mode():
339
+ return {"comments": []}
340
+ try:
341
+ config = get_github_config()
342
+ comments = await list_issue_comments(config, number)
343
+ return {"comments": comments}
344
+ except ValueError as e:
345
+ raise HTTPException(status_code=404, detail=str(e)) from None
346
+ except httpx.HTTPStatusError as e:
347
+ if e.response.status_code == 404:
348
+ raise HTTPException(status_code=404, detail="Issue not found") from None
349
+ if e.response.status_code in (401, 403):
350
+ raise HTTPException(
351
+ status_code=e.response.status_code,
352
+ detail="GitHub token is invalid or expired",
353
+ ) from None
354
+ raise HTTPException(status_code=502, detail="GitHub API error") from e
355
+ except Exception:
356
+ logger.exception("Failed to list comments for issue #%d", number)
357
+ raise HTTPException(status_code=500, detail="Internal server error") from None
358
+
359
+
360
+ @router.post("/{number}/comments", status_code=201)
361
+ async def create_issue_comment_endpoint(
362
+ number: int,
363
+ body: IssueCommentCreateBody,
364
+ _payload: dict = Depends(auth_dependency),
365
+ __: dict = Depends(require_role("admin", "editor")),
366
+ ):
367
+ """Post a comment on an issue."""
368
+ if settings.is_mock_mode():
369
+ raise HTTPException(
370
+ status_code=400,
371
+ detail="Mock mode: issue comment posting disabled",
372
+ )
373
+ try:
374
+ config = get_github_config()
375
+ return await create_issue_comment(config, number, body=body.body)
376
+ except ValueError as e:
377
+ raise HTTPException(status_code=404, detail=str(e)) from None
378
+ except httpx.HTTPStatusError as e:
379
+ if e.response.status_code == 404:
380
+ raise HTTPException(status_code=404, detail="Issue not found") from None
381
+ if e.response.status_code in (401, 403):
382
+ raise HTTPException(
383
+ status_code=e.response.status_code,
384
+ detail="GitHub token is invalid or expired",
385
+ ) from None
386
+ if e.response.status_code == 422:
387
+ raise HTTPException(status_code=422, detail="Invalid comment data") from None
388
+ raise HTTPException(status_code=502, detail="GitHub API error") from e
389
+ except Exception:
390
+ logger.exception("Failed to create comment on issue #%d", number)
391
+ raise HTTPException(status_code=500, detail="Internal server error") from None
@@ -45,10 +45,10 @@ router = APIRouter(prefix="/api/pr", tags=["pr"], dependencies=[Depends(auth_dep
45
45
 
46
46
  @router.get("/sessions")
47
47
  async def list_sessions(
48
- _: dict = Depends(require_role("admin")),
48
+ _: dict = Depends(auth_dependency),
49
49
  db: AsyncSession = Depends(get_db),
50
50
  ):
51
- """Get all active sessions (admin only)."""
51
+ """Get all active sessions for authenticated users (FileInfoPanel branch tab)."""
52
52
  if settings.is_mock_mode():
53
53
  return {"sessions": [], "mock": True}
54
54
  sessions = await get_all_active_sessions(db)
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import time
7
+ from typing import Literal
7
8
 
8
9
  import httpx
9
10
  import jwt
@@ -12,6 +13,8 @@ from app.config import settings
12
13
 
13
14
  logger = logging.getLogger(__name__)
14
15
 
16
+ GitHubWriteAuthor = Literal["bot", "pat", "mock"]
17
+
15
18
  _TOKEN_BUFFER_SECONDS = 300
16
19
  _JWT_EXPIRY_SECONDS = 600
17
20
 
@@ -28,12 +31,40 @@ def is_app_configured() -> bool:
28
31
  )
29
32
 
30
33
 
34
+ def github_write_author_mode() -> GitHubWriteAuthor:
35
+ """How PR/Issue creation will attribute authorship."""
36
+ if is_app_configured():
37
+ return "bot"
38
+ if settings.github_token:
39
+ return "pat"
40
+ return "mock"
41
+
42
+
43
+ def warn_if_pat_used_for_write(operation: str) -> None:
44
+ """Warn when a GitHub write will fall back to PAT instead of GitHub App."""
45
+ if is_app_configured():
46
+ return
47
+ if settings.github_token:
48
+ logger.warning(
49
+ "GitHub App is not configured. %s will be created under the "
50
+ "personal account of the GITHUB_TOKEN owner. "
51
+ "Set GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY, and "
52
+ "GITHUB_APP_INSTALLATION_ID to use the Bot account as author.",
53
+ operation,
54
+ )
55
+ return
56
+ logger.warning(
57
+ "GitHub App is not configured and GITHUB_TOKEN is empty. %s may fail.",
58
+ operation,
59
+ )
60
+
61
+
31
62
  def _generate_jwt() -> str:
32
63
  now = int(time.time())
33
64
  payload = {
34
65
  "iat": now - 60,
35
66
  "exp": now + _JWT_EXPIRY_SECONDS,
36
- "iss": settings.github_app_id,
67
+ "iss": str(settings.github_app_id),
37
68
  }
38
69
  private_key = settings.github_app_private_key.replace("\\n", "\n")
39
70
  return jwt.encode(payload, private_key, algorithm="RS256")
@@ -3,9 +3,11 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import logging
6
+ from urllib.parse import quote
6
7
 
7
8
  from app.http_client import get_github_client
8
9
  from app.services.github import _auth_headers, parse_link_next, resolve_token
10
+ from app.services.github_app_token import warn_if_pat_used_for_write
9
11
 
10
12
  logger = logging.getLogger(__name__)
11
13
 
@@ -21,6 +23,7 @@ async def list_issues(
21
23
  *,
22
24
  state: str = "open",
23
25
  labels: str | None = None,
26
+ assignee: str | None = None,
24
27
  per_page: int = 30,
25
28
  page: int = 1,
26
29
  ) -> list[dict]:
@@ -35,6 +38,8 @@ async def list_issues(
35
38
  }
36
39
  if labels:
37
40
  params["labels"] = labels
41
+ if assignee:
42
+ params["assignee"] = assignee
38
43
  token = await resolve_token(config)
39
44
  resp = await client.get(
40
45
  f"{_repo_url(config)}/issues",
@@ -71,6 +76,7 @@ async def create_issue(
71
76
  labels: list[str] | None = None,
72
77
  ) -> dict:
73
78
  """Create a new issue."""
79
+ warn_if_pat_used_for_write("Issue")
74
80
  client = await get_github_client()
75
81
  payload: dict = {"title": title, "body": body}
76
82
  if labels:
@@ -134,13 +140,62 @@ async def create_label(
134
140
  )
135
141
  resp.raise_for_status()
136
142
  data = resp.json()
143
+ return _format_label(data)
144
+
145
+
146
+ def _format_label(raw: dict) -> dict:
137
147
  return {
138
- "name": data["name"],
139
- "color": data.get("color", ""),
140
- "description": data.get("description", ""),
148
+ "name": raw["name"],
149
+ "color": raw.get("color", ""),
150
+ "description": raw.get("description", ""),
141
151
  }
142
152
 
143
153
 
154
+ def _label_url(config: dict, name: str) -> str:
155
+ return f"{_repo_url(config)}/labels/{quote(name, safe='')}"
156
+
157
+
158
+ async def update_label(
159
+ config: dict,
160
+ name: str,
161
+ *,
162
+ new_name: str | None = None,
163
+ color: str | None = None,
164
+ description: str | None = None,
165
+ ) -> dict:
166
+ """Update an existing repository label (rename, color, description)."""
167
+ payload: dict[str, str] = {}
168
+ if new_name is not None:
169
+ payload["new_name"] = new_name
170
+ if color is not None:
171
+ payload["color"] = color.lstrip("#")
172
+ if description is not None:
173
+ payload["description"] = description
174
+ if not payload:
175
+ raise ValueError("At least one field must be provided to update a label")
176
+
177
+ client = await get_github_client()
178
+ token = await resolve_token(config)
179
+ resp = await client.patch(
180
+ _label_url(config, name),
181
+ headers=_auth_headers(token),
182
+ json=payload,
183
+ )
184
+ resp.raise_for_status()
185
+ return _format_label(resp.json())
186
+
187
+
188
+ async def delete_label(config: dict, name: str) -> None:
189
+ """Delete a repository label."""
190
+ client = await get_github_client()
191
+ token = await resolve_token(config)
192
+ resp = await client.delete(
193
+ _label_url(config, name),
194
+ headers=_auth_headers(token),
195
+ )
196
+ resp.raise_for_status()
197
+
198
+
144
199
  async def list_issue_labels(config: dict) -> list[dict]:
145
200
  """List all labels for the repository (auto-paginates via Link header)."""
146
201
  token = await resolve_token(config)
@@ -156,14 +211,7 @@ async def list_issue_labels(config: dict) -> list[dict]:
156
211
  data = resp.json()
157
212
  if not isinstance(data, list):
158
213
  break
159
- all_labels.extend(
160
- {
161
- "name": label["name"],
162
- "color": label.get("color", ""),
163
- "description": label.get("description", ""),
164
- }
165
- for label in data
166
- )
214
+ all_labels.extend(_format_label(label) for label in data)
167
215
  url = parse_link_next(resp.headers.get("link"))
168
216
  params = {}
169
217
 
@@ -194,3 +242,45 @@ def _format_issue_detail(raw: dict) -> dict:
194
242
  result["body"] = raw.get("body") or ""
195
243
  result["url"] = raw.get("html_url", "")
196
244
  return result
245
+
246
+
247
+ async def list_issue_comments(config: dict, number: int) -> list[dict]:
248
+ """List comments on an issue (chronological)."""
249
+ token = await resolve_token(config)
250
+ client = await get_github_client()
251
+ resp = await client.get(
252
+ f"{_repo_url(config)}/issues/{number}/comments",
253
+ headers=_auth_headers(token),
254
+ params={"per_page": 100, "sort": "created", "direction": "asc"},
255
+ )
256
+ resp.raise_for_status()
257
+ data = resp.json()
258
+ if not isinstance(data, list):
259
+ return []
260
+ return [_format_issue_comment(c) for c in data]
261
+
262
+
263
+ async def create_issue_comment(config: dict, number: int, *, body: str) -> dict:
264
+ """Post a comment on an issue."""
265
+ warn_if_pat_used_for_write("Issue comment")
266
+ client = await get_github_client()
267
+ token = await resolve_token(config)
268
+ resp = await client.post(
269
+ f"{_repo_url(config)}/issues/{number}/comments",
270
+ headers=_auth_headers(token),
271
+ json={"body": body},
272
+ )
273
+ resp.raise_for_status()
274
+ return _format_issue_comment(resp.json())
275
+
276
+
277
+ def _format_issue_comment(raw: dict) -> dict:
278
+ """GitHub issue comment for API responses."""
279
+ return {
280
+ "id": raw["id"],
281
+ "body": raw.get("body") or "",
282
+ "author": (raw.get("user") or {}).get("login", ""),
283
+ "createdAt": raw.get("created_at", ""),
284
+ "updatedAt": raw.get("updated_at", ""),
285
+ "url": raw.get("html_url", ""),
286
+ }
@@ -8,7 +8,11 @@ from urllib.parse import quote
8
8
  from app.config import settings
9
9
  from app.http_client import get_github_client
10
10
  from app.services.github import parse_link_next
11
- from app.services.github_app_token import get_installation_token, is_app_configured
11
+ from app.services.github_app_token import (
12
+ get_installation_token,
13
+ is_app_configured,
14
+ warn_if_pat_used_for_write,
15
+ )
12
16
 
13
17
  logger = logging.getLogger(__name__)
14
18
 
@@ -334,6 +338,7 @@ async def create_pr(
334
338
 
335
339
  Uses GitHub App token when configured so the PR author is the bot.
336
340
  """
341
+ warn_if_pat_used_for_write("Pull request")
337
342
  token = await _resolve_token(config)
338
343
  headers = _headers_with_token(token)
339
344
  url = f"https://api.github.com/repos/{config['owner']}/{config['repo']}/pulls"
@@ -7,7 +7,9 @@ import pytest
7
7
  from app.services.github_app_token import (
8
8
  _generate_jwt,
9
9
  get_installation_token,
10
+ github_write_author_mode,
10
11
  is_app_configured,
12
+ warn_if_pat_used_for_write,
11
13
  )
12
14
 
13
15
 
@@ -41,6 +43,24 @@ class TestIsAppConfigured:
41
43
  assert is_app_configured() is False
42
44
 
43
45
 
46
+ class TestGithubWriteAuthorMode:
47
+ @patch("app.services.github_app_token.is_app_configured", return_value=True)
48
+ def test_bot_when_app_configured(self, _mock_app):
49
+ assert github_write_author_mode() == "bot"
50
+
51
+ @patch("app.services.github_app_token.is_app_configured", return_value=False)
52
+ @patch("app.services.github_app_token.settings")
53
+ def test_pat_when_token_only(self, mock_settings, _mock_app):
54
+ mock_settings.github_token = "ghp_test"
55
+ assert github_write_author_mode() == "pat"
56
+
57
+ @patch("app.services.github_app_token.is_app_configured", return_value=False)
58
+ @patch("app.services.github_app_token.settings")
59
+ def test_mock_when_unconfigured(self, mock_settings, _mock_app):
60
+ mock_settings.github_token = None
61
+ assert github_write_author_mode() == "mock"
62
+
63
+
44
64
  class TestGenerateJwt:
45
65
  @patch("app.services.github_app_token.settings")
46
66
  @patch("app.services.github_app_token.jwt")
@@ -57,7 +77,7 @@ class TestGenerateJwt:
57
77
  assert call_args[0][1] == "fake-key"
58
78
  assert call_args[1]["algorithm"] == "RS256"
59
79
  payload = call_args[0][0]
60
- assert payload["iss"] == 99
80
+ assert payload["iss"] == "99"
61
81
  assert "iat" in payload
62
82
  assert "exp" in payload
63
83
 
@@ -102,3 +122,31 @@ class TestGetInstallationToken:
102
122
 
103
123
  assert token == "ghs_installation_token"
104
124
  assert mod._cached_token == "ghs_installation_token"
125
+
126
+
127
+ class TestWarnIfPatUsedForWrite:
128
+ @patch("app.services.github_app_token.is_app_configured", return_value=True)
129
+ def test_no_warning_when_app_configured(self, _mock_app):
130
+ with patch("app.services.github_app_token.logger") as mock_logger:
131
+ warn_if_pat_used_for_write("Pull request")
132
+ mock_logger.warning.assert_not_called()
133
+
134
+ @patch("app.services.github_app_token.is_app_configured", return_value=False)
135
+ @patch("app.services.github_app_token.settings")
136
+ def test_warns_when_pat_fallback(self, mock_settings, _mock_app):
137
+ mock_settings.github_token = "ghp_test"
138
+ with patch("app.services.github_app_token.logger") as mock_logger:
139
+ warn_if_pat_used_for_write("Issue")
140
+ mock_logger.warning.assert_called_once()
141
+ message = mock_logger.warning.call_args[0][0]
142
+ assert "GITHUB_TOKEN owner" in message
143
+
144
+ @patch("app.services.github_app_token.is_app_configured", return_value=False)
145
+ @patch("app.services.github_app_token.settings")
146
+ def test_warns_when_no_credentials(self, mock_settings, _mock_app):
147
+ mock_settings.github_token = ""
148
+ with patch("app.services.github_app_token.logger") as mock_logger:
149
+ warn_if_pat_used_for_write("Pull request")
150
+ mock_logger.warning.assert_called_once()
151
+ message = mock_logger.warning.call_args[0][0]
152
+ assert "may fail" in message