@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.
- package/README.md +89 -94
- package/dist/godd.cjs +349 -233
- package/dist/godd.js +2193 -838
- package/dist/index.js +152 -122
- package/{notes-api → notes/api}/.env.example +6 -1
- package/{notes-api → notes/api}/README.md +6 -5
- package/{notes-api → notes/api}/app/main.py +29 -2
- package/{notes-api → notes/api}/app/routers/auth.py +6 -1
- package/{notes-api → notes/api}/app/routers/issues.py +157 -1
- package/{notes-api → notes/api}/app/routers/pr.py +2 -2
- package/{notes-api → notes/api}/app/services/github_app_token.py +32 -1
- package/{notes-api → notes/api}/app/services/github_issues.py +101 -11
- package/{notes-api → notes/api}/app/services/github_pr.py +6 -1
- package/{notes-api → notes/api}/tests/test_github_app_token.py +49 -1
- package/{notes-api → notes/api}/tests/test_github_service.py +42 -0
- package/notes/api/tests/test_health.py +52 -0
- package/{notes-api → notes/api}/tests/test_issues.py +216 -0
- package/{notes-api → notes/api}/tests/test_pr.py +24 -0
- package/{notes-api → notes/api}/tests/test_pr_chain.py +19 -0
- package/{notes-api → notes/api}/uv.lock +178 -2
- package/notes/app/DESIGN.md +273 -0
- package/{notes-app → notes/app}/README.md +4 -1
- package/notes/app/docker/Dockerfile.e2e +19 -0
- package/notes/app/docker/Dockerfile.prod +15 -0
- package/notes/app/docker/Dockerfile.test +17 -0
- package/notes/app/e2e/file-tree.spec.ts +26 -0
- package/notes/app/e2e/file-view.spec.ts +25 -0
- package/notes/app/e2e/fixtures.ts +28 -0
- package/{notes-app → notes/app}/e2e/global-setup.ts +5 -7
- package/notes/app/e2e/login.spec.ts +25 -0
- package/notes/app/e2e/pages/login-page.ts +29 -0
- package/notes/app/e2e/pages/main-page.ts +29 -0
- package/notes/app/e2e/panel-resize-handle.spec.ts +213 -0
- package/{notes-app → notes/app}/eslint.config.js +9 -0
- package/{notes-app → notes/app}/package.json +0 -3
- package/{notes-app → notes/app}/pnpm-workspace.yaml +1 -0
- package/notes/app/src/components/CommentPanel.test.tsx +194 -0
- package/{notes-app → notes/app}/src/components/CommentPanel.tsx +119 -10
- package/notes/app/src/components/CommentSelectionPopover.tsx +26 -0
- package/notes/app/src/components/EditToolbar.test.tsx +102 -0
- package/{notes-app → notes/app}/src/components/EditToolbar.tsx +5 -54
- package/notes/app/src/components/FileInfoPanel.branch-tab.test.tsx +250 -0
- package/notes/app/src/components/FileInfoPanel.history-tab.test.tsx +146 -0
- package/notes/app/src/components/FileInfoPanel.test.tsx +101 -0
- package/{notes-app → notes/app}/src/components/FileInfoPanel.tsx +135 -20
- package/notes/app/src/components/IssueCommentThread.test.tsx +93 -0
- package/notes/app/src/components/IssueCommentThread.tsx +156 -0
- package/notes/app/src/components/IssueDetailView.test.tsx +1118 -0
- package/notes/app/src/components/IssueDetailView.tsx +476 -0
- package/notes/app/src/components/IssueList.test.tsx +545 -0
- package/notes/app/src/components/IssueList.tsx +374 -0
- package/{notes-app → notes/app}/src/components/LabelSelector.test.tsx +94 -8
- package/notes/app/src/components/LabelSelector.tsx +452 -0
- package/{notes-app → notes/app}/src/components/MarkdownEditor.tsx +140 -2
- package/notes/app/src/components/PanelResizeHandle.test.tsx +481 -0
- package/notes/app/src/components/PanelResizeHandle.tsx +96 -0
- package/{notes-app → notes/app}/src/hooks/useEditSession.test.ts +171 -23
- package/{notes-app → notes/app}/src/hooks/useEditSession.ts +49 -62
- package/notes/app/src/hooks/useMdUpLayout.test.ts +10 -0
- package/notes/app/src/hooks/useMdUpLayout.ts +27 -0
- package/notes/app/src/hooks/usePeriodicSync.test.ts +244 -0
- package/notes/app/src/hooks/usePeriodicSync.ts +83 -0
- package/notes/app/src/hooks/useResizablePanelWidth.test.ts +251 -0
- package/notes/app/src/hooks/useResizablePanelWidth.ts +167 -0
- package/{notes-app → notes/app}/src/lib/api.test.ts +81 -1
- package/{notes-app → notes/app}/src/lib/api.ts +98 -4
- package/notes/app/src/lib/commentAnchor.test.ts +22 -0
- package/notes/app/src/lib/commentAnchor.ts +28 -0
- package/notes/app/src/lib/commentHighlight.test.ts +110 -0
- package/notes/app/src/lib/commentHighlight.ts +44 -0
- package/notes/app/src/lib/github-sync.test.ts +126 -0
- package/notes/app/src/lib/github-sync.ts +89 -0
- package/notes/app/src/lib/issue-list-utils.test.ts +275 -0
- package/notes/app/src/lib/issue-list-utils.ts +135 -0
- package/notes/app/src/lib/issue-mobile-layout.test.ts +17 -0
- package/notes/app/src/lib/issue-mobile-layout.ts +20 -0
- package/notes/app/src/lib/issue-url-sync.test.ts +73 -0
- package/notes/app/src/lib/issue-url-sync.ts +43 -0
- package/notes/app/src/lib/main-page-default-view.test.ts +37 -0
- package/notes/app/src/lib/main-page-default-view.ts +15 -0
- package/notes/app/src/lib/resizable-panel.test.ts +199 -0
- package/notes/app/src/lib/resizable-panel.ts +136 -0
- package/notes/app/src/pages/LoginPage.test.tsx +17 -0
- package/notes/app/src/pages/MainPage.branch-modal.test.tsx +226 -0
- package/notes/app/src/pages/MainPage.issue-sync.test.tsx +145 -0
- package/notes/app/src/pages/MainPage.sidebar.test.tsx +361 -0
- package/{notes-app → notes/app}/src/pages/MainPage.tsx +406 -61
- package/{notes-app → notes/app}/src/pages/SettingsPage.test.tsx +32 -23
- package/{notes-app → notes/app}/src/pages/UserManagementPage.test.tsx +8 -18
- package/notes/app/src/test/mocks.ts +156 -0
- package/notes/app/src/test/render.tsx +27 -0
- package/package.json +3 -5
- package/notes-api/tests/test_health.py +0 -11
- package/notes-app/docker/Dockerfile.e2e +0 -14
- package/notes-app/docker/Dockerfile.prod +0 -12
- package/notes-app/docker/Dockerfile.test +0 -12
- package/notes-app/e2e/file-tree.spec.ts +0 -29
- package/notes-app/e2e/file-view.spec.ts +0 -24
- package/notes-app/e2e/login.spec.ts +0 -32
- package/notes-app/src/components/IssueDetailView.test.tsx +0 -201
- package/notes-app/src/components/IssueDetailView.tsx +0 -318
- package/notes-app/src/components/IssueList.test.tsx +0 -93
- package/notes-app/src/components/IssueList.tsx +0 -169
- package/notes-app/src/components/LabelSelector.tsx +0 -276
- package/notes-app/src/pages/LoginPage.test.tsx +0 -29
- package/stacks/django-vue.yaml +0 -33
- package/stacks/fastapi-react.yaml +0 -160
- package/stacks/flutter-firebase.yaml +0 -58
- package/stacks/nextjs-prisma.yaml +0 -33
- package/stacks/schema.json +0 -144
- package/templates/agents/architect.hbs +0 -183
- package/templates/agents/auto-documenter.hbs +0 -36
- package/templates/agents/backend-developer.hbs +0 -175
- package/templates/agents/code-reviewer.hbs +0 -29
- package/templates/agents/database-developer.hbs +0 -52
- package/templates/agents/debug-specialist.hbs +0 -39
- package/templates/agents/environment-setup.hbs +0 -41
- package/templates/agents/frontend-developer.hbs +0 -45
- package/templates/agents/integration-qa.hbs +0 -26
- package/templates/agents/performance-qa.hbs +0 -24
- package/templates/agents/pr-writer.hbs +0 -53
- package/templates/agents/quality-lead.hbs +0 -26
- package/templates/agents/requirements-analyst.hbs +0 -26
- package/templates/agents/security-analyst.hbs +0 -21
- package/templates/agents/security-reviewer.hbs +0 -20
- package/templates/agents/ssot-updater.hbs +0 -45
- package/templates/agents/technical-writer.hbs +0 -28
- package/templates/agents/unit-test-engineer.hbs +0 -19
- package/templates/agents/user-clone.hbs +0 -43
- package/templates/github-actions/aws/deploy-notes.yml.hbs +0 -117
- package/templates/github-actions/azure/deploy-notes.yml.hbs +0 -78
- package/templates/github-actions/gcp/deploy-notes.yml.hbs +0 -76
- package/templates/github-actions/vercel-railway/deploy-notes.yml.hbs +0 -49
- package/templates/mindsets/agent-stdio.hbs +0 -51
- package/templates/mindsets/dart.hbs +0 -39
- package/templates/mindsets/ddd-clean-architecture.hbs +0 -52
- package/templates/mindsets/electron.hbs +0 -41
- package/templates/mindsets/fastapi.hbs +0 -40
- package/templates/mindsets/flutter.hbs +0 -56
- package/templates/mindsets/kotlin.hbs +0 -46
- package/templates/mindsets/postgresql.hbs +0 -39
- package/templates/mindsets/python.hbs +0 -39
- package/templates/mindsets/react.hbs +0 -51
- package/templates/mindsets/swift.hbs +0 -46
- package/templates/mindsets/terraform.hbs +0 -53
- package/templates/mindsets/typescript.hbs +0 -34
- package/templates/mindsets/vite.hbs +0 -37
- package/templates/notes-compose.yml +0 -82
- package/templates/partials/cogito-geo.hbs +0 -84
- package/templates/partials/cogito-godd.hbs +0 -22
- package/templates/partials/cogito-grammar.hbs +0 -105
- package/templates/partials/cogito-particle.hbs +0 -76
- package/templates/partials/cogito-root-ai.hbs +0 -79
- package/templates/partials/cogito-root-arch.hbs +0 -75
- package/templates/partials/cogito-root-core.hbs +0 -75
- package/templates/partials/cogito-root-daily.hbs +0 -58
- package/templates/partials/cogito-root-dev.hbs +0 -71
- package/templates/partials/cogito-root-devops.hbs +0 -71
- package/templates/partials/cogito-root-framework.hbs +0 -80
- package/templates/partials/cogito-root-lang.hbs +0 -74
- package/templates/partials/godd-core.hbs +0 -50
- package/templates/partials/project-context.hbs +0 -52
- package/templates/partials/ssot-header.hbs +0 -40
- package/templates/partials/ssot-rules.hbs +0 -30
- package/templates/prompts/adr.hbs +0 -39
- package/templates/prompts/agent-dev-workflow.hbs +0 -34
- package/templates/prompts/analyze-report.hbs +0 -36
- package/templates/prompts/auto-documentation.hbs +0 -37
- package/templates/prompts/check.hbs +0 -52
- package/templates/prompts/commit.hbs +0 -96
- package/templates/prompts/dev.hbs +0 -127
- package/templates/prompts/docs-init.hbs +0 -101
- package/templates/prompts/docs-update.hbs +0 -51
- package/templates/prompts/git-workflow.hbs +0 -125
- package/templates/prompts/impact-analysis.hbs +0 -43
- package/templates/prompts/install.hbs +0 -40
- package/templates/prompts/new-branch.hbs +0 -61
- package/templates/prompts/notes-deploy.hbs +0 -138
- package/templates/prompts/pr-analyze.hbs +0 -44
- package/templates/prompts/pr-create.hbs +0 -125
- package/templates/prompts/push-execute.hbs +0 -83
- package/templates/prompts/push.hbs +0 -24
- package/templates/prompts/release-notes.hbs +0 -38
- package/templates/prompts/requirements-to-business-flow.hbs +0 -29
- package/templates/prompts/requirements-to-tickets.hbs +0 -26
- package/templates/prompts/review.hbs +0 -126
- package/templates/prompts/setup.hbs +0 -247
- package/templates/prompts/spec.hbs +0 -35
- package/templates/prompts/specification-to-tickets.hbs +0 -24
- package/templates/prompts/submit.hbs +0 -143
- package/templates/prompts/sync.hbs +0 -63
- package/templates/prompts/test-plan.hbs +0 -56
- package/templates/scripts/aws/terraform-backend-setup.sh.hbs +0 -149
- package/templates/terraform/aws/alb.tf.hbs +0 -58
- package/templates/terraform/aws/backend.tf.hbs +0 -10
- package/templates/terraform/aws/cloudfront.tf.hbs +0 -96
- package/templates/terraform/aws/ecr.tf.hbs +0 -30
- package/templates/terraform/aws/ecs.tf.hbs +0 -142
- package/templates/terraform/aws/iam.tf.hbs +0 -88
- package/templates/terraform/aws/local.tf.hbs +0 -34
- package/templates/terraform/aws/output.tf.hbs +0 -49
- package/templates/terraform/aws/provider.tf.hbs +0 -26
- package/templates/terraform/aws/rds.tf.hbs +0 -38
- package/templates/terraform/aws/s3.tf.hbs +0 -34
- package/templates/terraform/aws/secrets.tf.hbs +0 -53
- package/templates/terraform/aws/security_groups.tf.hbs +0 -81
- package/templates/terraform/aws/vpc.tf.hbs +0 -63
- package/templates/terraform/azure/backend.tf.hbs +0 -8
- package/templates/terraform/azure/iam.tf.hbs +0 -36
- package/templates/terraform/azure/main.tf.hbs +0 -203
- package/templates/terraform/azure/output.tf.hbs +0 -19
- package/templates/terraform/azure/provider.tf.hbs +0 -19
- package/templates/terraform/gcp/backend.tf.hbs +0 -6
- package/templates/terraform/gcp/iam.tf.hbs +0 -47
- package/templates/terraform/gcp/main.tf.hbs +0 -175
- package/templates/terraform/gcp/output.tf.hbs +0 -15
- package/templates/terraform/gcp/provider.tf.hbs +0 -19
- package/templates/terraform/vercel-railway/main.tf.hbs +0 -64
- package/templates/terraform/vercel-railway/output.tf.hbs +0 -11
- /package/{notes-api → notes/api}/.dockerignore +0 -0
- /package/{notes-api → notes/api}/alembic/README +0 -0
- /package/{notes-api → notes/api}/alembic/env.py +0 -0
- /package/{notes-api → notes/api}/alembic/script.py.mako +0 -0
- /package/{notes-api → notes/api}/alembic/versions/001_initial_notes_tables.py +0 -0
- /package/{notes-api → notes/api}/alembic/versions/002_add_performance_indexes.py +0 -0
- /package/{notes-api → notes/api}/alembic/versions/003_add_yjs_documents.py +0 -0
- /package/{notes-api → notes/api}/alembic/versions/004_add_comments.py +0 -0
- /package/{notes-api → notes/api}/alembic/versions/005_add_github_username.py +0 -0
- /package/{notes-api → notes/api}/alembic/versions/006_add_check_constraints.py +0 -0
- /package/{notes-api → notes/api}/alembic.ini +0 -0
- /package/{notes-api → notes/api}/app/__init__.py +0 -0
- /package/{notes-api → notes/api}/app/config.py +0 -0
- /package/{notes-api → notes/api}/app/database.py +0 -0
- /package/{notes-api → notes/api}/app/http_client.py +0 -0
- /package/{notes-api → notes/api}/app/models/__init__.py +0 -0
- /package/{notes-api → notes/api}/app/models/audit.py +0 -0
- /package/{notes-api → notes/api}/app/models/base.py +0 -0
- /package/{notes-api → notes/api}/app/models/collab.py +0 -0
- /package/{notes-api → notes/api}/app/models/comment.py +0 -0
- /package/{notes-api → notes/api}/app/models/session.py +0 -0
- /package/{notes-api → notes/api}/app/models/setting.py +0 -0
- /package/{notes-api → notes/api}/app/models/user.py +0 -0
- /package/{notes-api → notes/api}/app/path_validation.py +0 -0
- /package/{notes-api → notes/api}/app/routers/__init__.py +0 -0
- /package/{notes-api → notes/api}/app/routers/collab.py +0 -0
- /package/{notes-api → notes/api}/app/routers/comments.py +0 -0
- /package/{notes-api → notes/api}/app/routers/files.py +0 -0
- /package/{notes-api → notes/api}/app/routers/settings.py +0 -0
- /package/{notes-api → notes/api}/app/routers/tree.py +0 -0
- /package/{notes-api → notes/api}/app/routers/upload.py +0 -0
- /package/{notes-api → notes/api}/app/routers/users.py +0 -0
- /package/{notes-api → notes/api}/app/schemas/__init__.py +0 -0
- /package/{notes-api → notes/api}/app/schemas/auth.py +0 -0
- /package/{notes-api → notes/api}/app/schemas/user.py +0 -0
- /package/{notes-api → notes/api}/app/security.py +0 -0
- /package/{notes-api → notes/api}/app/services/__init__.py +0 -0
- /package/{notes-api → notes/api}/app/services/audit.py +0 -0
- /package/{notes-api → notes/api}/app/services/business_date.py +0 -0
- /package/{notes-api → notes/api}/app/services/collab.py +0 -0
- /package/{notes-api → notes/api}/app/services/github.py +0 -0
- /package/{notes-api → notes/api}/app/services/pr_chain.py +0 -0
- /package/{notes-api → notes/api}/app/startup.py +0 -0
- /package/{notes-api → notes/api}/docker/Dockerfile +0 -0
- /package/{notes-api → notes/api}/docker/Dockerfile.test +0 -0
- /package/{notes-api → notes/api}/pyproject.toml +0 -0
- /package/{notes-api → notes/api}/tests/conftest.py +0 -0
- /package/{notes-api → notes/api}/tests/test_audit.py +0 -0
- /package/{notes-api → notes/api}/tests/test_auth.py +0 -0
- /package/{notes-api → notes/api}/tests/test_business_date.py +0 -0
- /package/{notes-api → notes/api}/tests/test_collab.py +0 -0
- /package/{notes-api → notes/api}/tests/test_comments.py +0 -0
- /package/{notes-api → notes/api}/tests/test_config_ssl.py +0 -0
- /package/{notes-api → notes/api}/tests/test_config_warnings.py +0 -0
- /package/{notes-api → notes/api}/tests/test_files.py +0 -0
- /package/{notes-api → notes/api}/tests/test_path_validation.py +0 -0
- /package/{notes-api → notes/api}/tests/test_security.py +0 -0
- /package/{notes-api → notes/api}/tests/test_settings.py +0 -0
- /package/{notes-api → notes/api}/tests/test_tree.py +0 -0
- /package/{notes-api → notes/api}/tests/test_upload.py +0 -0
- /package/{notes-api → notes/api}/tests/test_users.py +0 -0
- /package/{notes-app → notes/app}/.dockerignore +0 -0
- /package/{notes-app → notes/app}/index.html +0 -0
- /package/{notes-app → notes/app}/nginx.conf +0 -0
- /package/{notes-app → notes/app}/package-lock.json +0 -0
- /package/{notes-app → notes/app}/playwright.config.ts +0 -0
- /package/{notes-app → notes/app}/pnpm-lock.yaml +0 -0
- /package/{notes-app → notes/app}/src/App.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/BranchSelectorModal.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/CodeBlockView.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/CodeBlockView.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/CollabStatus.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/DiffPreview.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/DrawioEditor.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/DrawioViewer.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/ErrorBoundary.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/ErrorBoundary.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/FileContentView.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/FileContextMenu.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/FileContextMenu.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/FileDialogs.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/FileDialogs.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/SaveStatusIndicator.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/SaveStatusIndicator.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/Skeleton.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/SpreadsheetEditor.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/SpreadsheetEditor.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/TextEditor.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/ToastContainer.tsx +0 -0
- /package/{notes-app → notes/app}/src/components/TreeNode.tsx +0 -0
- /package/{notes-app → notes/app}/src/contexts/AuthContext.test.tsx +0 -0
- /package/{notes-app → notes/app}/src/contexts/AuthContext.tsx +0 -0
- /package/{notes-app → notes/app}/src/contexts/ToastContext.test.ts +0 -0
- /package/{notes-app → notes/app}/src/contexts/ToastContext.tsx +0 -0
- /package/{notes-app → notes/app}/src/hooks/useAutoSave.test.ts +0 -0
- /package/{notes-app → notes/app}/src/hooks/useAutoSave.ts +0 -0
- /package/{notes-app → notes/app}/src/hooks/useCollaboration.ts +0 -0
- /package/{notes-app → notes/app}/src/hooks/useComments.ts +0 -0
- /package/{notes-app → notes/app}/src/hooks/useYText.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/branch-diff.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/branch-diff.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/created-file.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/created-file.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/csv-utils.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/csv-utils.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/deepLinkPathToSearch.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/deepLinkPathToSearch.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/draft-storage.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/draft-storage.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/mermaid-render.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/postLoginRedirect.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/postLoginRedirect.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/tree-utils.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/user-colors.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/xlsx-utils.test.ts +0 -0
- /package/{notes-app → notes/app}/src/lib/xlsx-utils.ts +0 -0
- /package/{notes-app → notes/app}/src/main.tsx +0 -0
- /package/{notes-app → notes/app}/src/pages/LoginPage.tsx +0 -0
- /package/{notes-app → notes/app}/src/pages/MainPage.insertChildEntry.test.ts +0 -0
- /package/{notes-app → notes/app}/src/pages/SettingsPage.tsx +0 -0
- /package/{notes-app → notes/app}/src/pages/UserManagementPage.tsx +0 -0
- /package/{notes-app → notes/app}/src/styles/globals.css +0 -0
- /package/{notes-app → notes/app}/src/types.ts +0 -0
- /package/{notes-app → notes/app}/src/vite-env.d.ts +0 -0
- /package/{notes-app → notes/app}/tsconfig.json +0 -0
- /package/{notes-app → notes/app}/tsconfig.node.json +0 -0
- /package/{notes-app → notes/app}/vite.config.js +0 -0
- /package/{notes-app → notes/app}/vite.config.ts +0 -0
- /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 PAT
|
|
43
|
-
| GITHUB_APP_ID |
|
|
44
|
-
| GITHUB_APP_PRIVATE_KEY |
|
|
45
|
-
| 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 {
|
|
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
|
|
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,
|
|
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(
|
|
48
|
+
_: dict = Depends(auth_dependency),
|
|
49
49
|
db: AsyncSession = Depends(get_db),
|
|
50
50
|
):
|
|
51
|
-
"""Get all active sessions (
|
|
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":
|
|
139
|
-
"color":
|
|
140
|
-
"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
|
|
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
|