shappetrack 1.0.0__tar.gz

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 (208) hide show
  1. shappetrack-1.0.0/.dockerignore +48 -0
  2. shappetrack-1.0.0/.env.example +107 -0
  3. shappetrack-1.0.0/.github/workflows/deploy.yml +312 -0
  4. shappetrack-1.0.0/.gitignore +42 -0
  5. shappetrack-1.0.0/.python-version +1 -0
  6. shappetrack-1.0.0/Dockerfile +84 -0
  7. shappetrack-1.0.0/PKG-INFO +84 -0
  8. shappetrack-1.0.0/README.md +1469 -0
  9. shappetrack-1.0.0/__init__.py +0 -0
  10. shappetrack-1.0.0/alembic/README +1 -0
  11. shappetrack-1.0.0/alembic/env.py +79 -0
  12. shappetrack-1.0.0/alembic/script.py.mako +26 -0
  13. shappetrack-1.0.0/alembic/versions/05d4ee5e7fb1_initial_schema_all_tables.py +950 -0
  14. shappetrack-1.0.0/alembic/versions/a1b2c3d4e5f6_issue_pin_lock_due_date_time_entries.py +76 -0
  15. shappetrack-1.0.0/alembic/versions/b2c3d4e5f6a7_pr_pin_lock_due_date_time_entries.py +73 -0
  16. shappetrack-1.0.0/alembic/versions/c3d4e5f6a7b8_add_repo_extended_fields.py +36 -0
  17. shappetrack-1.0.0/alembic/versions/d1e2f3a4b5c6_add_projects_and_notification_prefs.py +79 -0
  18. shappetrack-1.0.0/alembic/versions/e2f3a4b5c6d7_avatar_url_to_text.py +28 -0
  19. shappetrack-1.0.0/alembic/versions/f3a4b5c6d7e8_add_chat_message_is_pinned.py +30 -0
  20. shappetrack-1.0.0/alembic/versions/g4b5c6d7e8f9_add_sprints_and_issue_sprint_fields.py +63 -0
  21. shappetrack-1.0.0/alembic/versions/h5c6d7e8f9a0_add_sla_configs.py +40 -0
  22. shappetrack-1.0.0/alembic/versions/i6d7e8f9a0b1_reset_tokens_and_issue_uniq.py +59 -0
  23. shappetrack-1.0.0/alembic/versions/j7e8f9a0b1c2_orphan_tables_runners_oauth_votes_mergequeue.py +99 -0
  24. shappetrack-1.0.0/alembic/versions/k8f9a0b1c2d3_webauthn_credentials_and_challenges.py +63 -0
  25. shappetrack-1.0.0/alembic.ini +116 -0
  26. shappetrack-1.0.0/api/__init__.py +3 -0
  27. shappetrack-1.0.0/api/v1/__init__.py +143 -0
  28. shappetrack-1.0.0/api/v1/admin.py +476 -0
  29. shappetrack-1.0.0/api/v1/analytics.py +425 -0
  30. shappetrack-1.0.0/api/v1/auth.py +629 -0
  31. shappetrack-1.0.0/api/v1/branch_protection.py +343 -0
  32. shappetrack-1.0.0/api/v1/branches.py +220 -0
  33. shappetrack-1.0.0/api/v1/changelog.py +139 -0
  34. shappetrack-1.0.0/api/v1/chat.py +527 -0
  35. shappetrack-1.0.0/api/v1/cli.py +46 -0
  36. shappetrack-1.0.0/api/v1/collaborators.py +362 -0
  37. shappetrack-1.0.0/api/v1/commits.py +331 -0
  38. shappetrack-1.0.0/api/v1/diffs.py +433 -0
  39. shappetrack-1.0.0/api/v1/feedback.py +103 -0
  40. shappetrack-1.0.0/api/v1/files.py +840 -0
  41. shappetrack-1.0.0/api/v1/gateway/__init__.py +1 -0
  42. shappetrack-1.0.0/api/v1/gateway/actions_settings.py +262 -0
  43. shappetrack-1.0.0/api/v1/gateway/avatars.py +42 -0
  44. shappetrack-1.0.0/api/v1/gateway/deps.py +37 -0
  45. shappetrack-1.0.0/api/v1/gateway/diffs.py +144 -0
  46. shappetrack-1.0.0/api/v1/gateway/explore.py +98 -0
  47. shappetrack-1.0.0/api/v1/gateway/extras.py +324 -0
  48. shappetrack-1.0.0/api/v1/gateway/issues.py +658 -0
  49. shappetrack-1.0.0/api/v1/gateway/milestones.py +669 -0
  50. shappetrack-1.0.0/api/v1/gateway/organizations.py +705 -0
  51. shappetrack-1.0.0/api/v1/gateway/pulls.py +515 -0
  52. shappetrack-1.0.0/api/v1/gateway/repo_admin.py +639 -0
  53. shappetrack-1.0.0/api/v1/gateway/repositories.py +1018 -0
  54. shappetrack-1.0.0/api/v1/gateway/schemas.py +362 -0
  55. shappetrack-1.0.0/api/v1/gateway/search.py +337 -0
  56. shappetrack-1.0.0/api/v1/gateway/translate.py +266 -0
  57. shappetrack-1.0.0/api/v1/gateway/users.py +615 -0
  58. shappetrack-1.0.0/api/v1/gateway/wiki.py +207 -0
  59. shappetrack-1.0.0/api/v1/gateway/workflows.py +664 -0
  60. shappetrack-1.0.0/api/v1/git_operations.py +199 -0
  61. shappetrack-1.0.0/api/v1/git_proxy.py +257 -0
  62. shappetrack-1.0.0/api/v1/issues.py +1075 -0
  63. shappetrack-1.0.0/api/v1/merge_queue.py +120 -0
  64. shappetrack-1.0.0/api/v1/milestones.py +514 -0
  65. shappetrack-1.0.0/api/v1/missedtask.py +466 -0
  66. shappetrack-1.0.0/api/v1/notifications.py +181 -0
  67. shappetrack-1.0.0/api/v1/organizations.py +605 -0
  68. shappetrack-1.0.0/api/v1/projects.py +296 -0
  69. shappetrack-1.0.0/api/v1/pull_requests.py +1042 -0
  70. shappetrack-1.0.0/api/v1/releases.py +552 -0
  71. shappetrack-1.0.0/api/v1/repositories.py +533 -0
  72. shappetrack-1.0.0/api/v1/safety.py +263 -0
  73. shappetrack-1.0.0/api/v1/search.py +646 -0
  74. shappetrack-1.0.0/api/v1/sla.py +158 -0
  75. shappetrack-1.0.0/api/v1/sprints.py +228 -0
  76. shappetrack-1.0.0/api/v1/ssh_keys.py +518 -0
  77. shappetrack-1.0.0/api/v1/stars.py +260 -0
  78. shappetrack-1.0.0/api/v1/teams.py +953 -0
  79. shappetrack-1.0.0/api/v1/tokens.py +110 -0
  80. shappetrack-1.0.0/api/v1/totp_utils.py +19 -0
  81. shappetrack-1.0.0/api/v1/two_factor.py +638 -0
  82. shappetrack-1.0.0/api/v1/user_follows.py +253 -0
  83. shappetrack-1.0.0/api/v1/users.py +929 -0
  84. shappetrack-1.0.0/api/v1/vcs.py +242 -0
  85. shappetrack-1.0.0/api/v1/votes.py +106 -0
  86. shappetrack-1.0.0/api/v1/watch.py +341 -0
  87. shappetrack-1.0.0/api/v1/webhooks.py +366 -0
  88. shappetrack-1.0.0/api/v1/workflows.py +500 -0
  89. shappetrack-1.0.0/auth/__init__.py +14 -0
  90. shappetrack-1.0.0/auth/engine_client.py +131 -0
  91. shappetrack-1.0.0/auth/jwt_handler.py +119 -0
  92. shappetrack-1.0.0/auth/password.py +45 -0
  93. shappetrack-1.0.0/auth/permissions.py +150 -0
  94. shappetrack-1.0.0/auth/principal.py +92 -0
  95. shappetrack-1.0.0/auth/scopes.py +54 -0
  96. shappetrack-1.0.0/auth/token_utils.py +50 -0
  97. shappetrack-1.0.0/config.py +200 -0
  98. shappetrack-1.0.0/core/__init__.py +13 -0
  99. shappetrack-1.0.0/core/access.py +41 -0
  100. shappetrack-1.0.0/core/audit.py +27 -0
  101. shappetrack-1.0.0/core/bootstrap.py +64 -0
  102. shappetrack-1.0.0/core/chat_events.py +78 -0
  103. shappetrack-1.0.0/core/chat_service.py +215 -0
  104. shappetrack-1.0.0/core/chat_ws.py +53 -0
  105. shappetrack-1.0.0/core/diff.py +238 -0
  106. shappetrack-1.0.0/core/email_service.py +70 -0
  107. shappetrack-1.0.0/core/encryption.py +150 -0
  108. shappetrack-1.0.0/core/git_server.py +332 -0
  109. shappetrack-1.0.0/core/logging.py +280 -0
  110. shappetrack-1.0.0/core/merge_service.py +486 -0
  111. shappetrack-1.0.0/core/notification_service.py +406 -0
  112. shappetrack-1.0.0/core/rate_limit.py +5 -0
  113. shappetrack-1.0.0/core/safety_guards.py +338 -0
  114. shappetrack-1.0.0/core/vcs.py +259 -0
  115. shappetrack-1.0.0/database.py +71 -0
  116. shappetrack-1.0.0/docker-compose.yml +144 -0
  117. shappetrack-1.0.0/docker-entrypoint.sh +49 -0
  118. shappetrack-1.0.0/docs/ci-runner-security.md +66 -0
  119. shappetrack-1.0.0/docs/config-inventory.md +92 -0
  120. shappetrack-1.0.0/docs/ssh-git-deployment.md +94 -0
  121. shappetrack-1.0.0/main.py +169 -0
  122. shappetrack-1.0.0/migrate_chat.py +45 -0
  123. shappetrack-1.0.0/migrate_to_engine.py +304 -0
  124. shappetrack-1.0.0/models/__init__.py +136 -0
  125. shappetrack-1.0.0/models/audit_log.py +55 -0
  126. shappetrack-1.0.0/models/branch.py +43 -0
  127. shappetrack-1.0.0/models/branch_protection.py +96 -0
  128. shappetrack-1.0.0/models/chat_channel.py +25 -0
  129. shappetrack-1.0.0/models/chat_member.py +17 -0
  130. shappetrack-1.0.0/models/chat_message.py +38 -0
  131. shappetrack-1.0.0/models/chat_reaction.py +15 -0
  132. shappetrack-1.0.0/models/commit.py +66 -0
  133. shappetrack-1.0.0/models/file_blob.py +47 -0
  134. shappetrack-1.0.0/models/issue.py +82 -0
  135. shappetrack-1.0.0/models/issue_comment.py +33 -0
  136. shappetrack-1.0.0/models/issue_reaction.py +35 -0
  137. shappetrack-1.0.0/models/issue_time_entry.py +26 -0
  138. shappetrack-1.0.0/models/issue_vote.py +16 -0
  139. shappetrack-1.0.0/models/merge_queue_entry.py +18 -0
  140. shappetrack-1.0.0/models/milestone.py +77 -0
  141. shappetrack-1.0.0/models/notification.py +125 -0
  142. shappetrack-1.0.0/models/oauth_application.py +47 -0
  143. shappetrack-1.0.0/models/organization.py +58 -0
  144. shappetrack-1.0.0/models/organization_member.py +66 -0
  145. shappetrack-1.0.0/models/password_reset_token.py +21 -0
  146. shappetrack-1.0.0/models/personal_access_token.py +57 -0
  147. shappetrack-1.0.0/models/pr_comment.py +39 -0
  148. shappetrack-1.0.0/models/pr_time_entry.py +22 -0
  149. shappetrack-1.0.0/models/project_board.py +27 -0
  150. shappetrack-1.0.0/models/project_card.py +22 -0
  151. shappetrack-1.0.0/models/project_column.py +27 -0
  152. shappetrack-1.0.0/models/pull_request.py +85 -0
  153. shappetrack-1.0.0/models/release.py +121 -0
  154. shappetrack-1.0.0/models/repo_meta.py +17 -0
  155. shappetrack-1.0.0/models/repo_notification_pref.py +30 -0
  156. shappetrack-1.0.0/models/repository.py +114 -0
  157. shappetrack-1.0.0/models/repository_collaborator.py +122 -0
  158. shappetrack-1.0.0/models/repository_label.py +30 -0
  159. shappetrack-1.0.0/models/repository_star.py +31 -0
  160. shappetrack-1.0.0/models/repository_watch.py +35 -0
  161. shappetrack-1.0.0/models/runner.py +58 -0
  162. shappetrack-1.0.0/models/session.py +46 -0
  163. shappetrack-1.0.0/models/sla_config.py +17 -0
  164. shappetrack-1.0.0/models/sprint.py +27 -0
  165. shappetrack-1.0.0/models/sprint_report.py +17 -0
  166. shappetrack-1.0.0/models/ssh_key.py +52 -0
  167. shappetrack-1.0.0/models/tag.py +49 -0
  168. shappetrack-1.0.0/models/team.py +52 -0
  169. shappetrack-1.0.0/models/team_member.py +44 -0
  170. shappetrack-1.0.0/models/team_repository_permission.py +47 -0
  171. shappetrack-1.0.0/models/two_factor_auth.py +34 -0
  172. shappetrack-1.0.0/models/user.py +143 -0
  173. shappetrack-1.0.0/models/user_follow.py +32 -0
  174. shappetrack-1.0.0/models/webauthn_challenge.py +25 -0
  175. shappetrack-1.0.0/models/webauthn_credential.py +45 -0
  176. shappetrack-1.0.0/models/webhook.py +45 -0
  177. shappetrack-1.0.0/models/workflow.py +54 -0
  178. shappetrack-1.0.0/models/workflow_job.py +65 -0
  179. shappetrack-1.0.0/models/workflow_run.py +67 -0
  180. shappetrack-1.0.0/models/workflow_step.py +66 -0
  181. shappetrack-1.0.0/pyproject.toml +40 -0
  182. shappetrack-1.0.0/pytest.ini +12 -0
  183. shappetrack-1.0.0/requirements-dev.txt +8 -0
  184. shappetrack-1.0.0/requirements.txt +55 -0
  185. shappetrack-1.0.0/scripts/create-engine-db.sql +5 -0
  186. shappetrack-1.0.0/scripts/generate_secret.py +5 -0
  187. shappetrack-1.0.0/scripts/make_admin.py +50 -0
  188. shappetrack-1.0.0/scripts/migrations/add_engine_columns.sql +4 -0
  189. shappetrack-1.0.0/scripts/migrations/add_repo_meta.sql +11 -0
  190. shappetrack-1.0.0/seed_data.py +117 -0
  191. shappetrack-1.0.0/shappetrack/README.md +67 -0
  192. shappetrack-1.0.0/shappetrack/RELEASING.md +89 -0
  193. shappetrack-1.0.0/shappetrack/__init__.py +7 -0
  194. shappetrack-1.0.0/shappetrack/__main__.py +4 -0
  195. shappetrack-1.0.0/shappetrack/__pyinstaller_entry.py +10 -0
  196. shappetrack-1.0.0/shappetrack/local.py +121 -0
  197. shappetrack-1.0.0/shappetrack/shapetrack.py +266 -0
  198. shappetrack-1.0.0/task-definition.json +207 -0
  199. shappetrack-1.0.0/tests/__init__.py +1 -0
  200. shappetrack-1.0.0/tests/conftest.py +74 -0
  201. shappetrack-1.0.0/tests/test_chat.py +43 -0
  202. shappetrack-1.0.0/tests/test_principal.py +65 -0
  203. shappetrack-1.0.0/tests/test_scopes.py +31 -0
  204. shappetrack-1.0.0/tests/test_secrets.py +54 -0
  205. shappetrack-1.0.0/tests/test_sessions.py +40 -0
  206. shappetrack-1.0.0/tests/test_token_utils.py +43 -0
  207. shappetrack-1.0.0/tests/test_tokens_api.py +57 -0
  208. shappetrack-1.0.0/wipe_all.py +130 -0
@@ -0,0 +1,48 @@
1
+ # Secrets — must NEVER be baked into the image (injected at runtime instead).
2
+ # .env exists in the repo; config.py reads it if present.
3
+ .env
4
+ .env.*
5
+ docs/engine-credentials.local.md
6
+
7
+ # Runtime data / state — belongs on a volume, not in the image.
8
+ # storage/git = bare repos (config.py), storage/blobs = release uploads (releases.py)
9
+ storage/
10
+ logs/
11
+ *.db
12
+ *.sqlite
13
+ *.sqlite3
14
+ *.log
15
+
16
+ # Version control
17
+ .git
18
+ .gitignore
19
+
20
+ # Python caches / virtualenvs (rebuilt/irrelevant in the image)
21
+ venv/
22
+ .venv/
23
+ env/
24
+ __pycache__/
25
+ *.py[cod]
26
+ *.pyo
27
+ *.egg-info/
28
+ .pytest_cache/
29
+ .mypy_cache/
30
+ .ruff_cache/
31
+
32
+ # Local tooling / editor
33
+ .idea/
34
+ .vscode/
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Throwaway migration backup created during prod-readiness work
39
+ _migration_backup/
40
+
41
+ # Tests are not needed in the production image (pytest.ini, tests/)
42
+ tests/
43
+ pytest.ini
44
+
45
+ # Docker build context self-reference (not needed inside the image)
46
+ Dockerfile
47
+ docker-compose.yml
48
+ .dockerignore
@@ -0,0 +1,107 @@
1
+ # ============================================================
2
+ # SECRETS — generate strong values before any non-dev use:
3
+ # python scripts/generate_secret.py
4
+ # In production (ENVIRONMENT=production) the app REFUSES to start
5
+ # if SECRET_KEY / ENCRYPTION_KEY are empty or left at dev defaults.
6
+ # ============================================================
7
+
8
+ # Server Configuration
9
+ APP_NAME=ShapeTrack
10
+ APP_VERSION=1.0.0
11
+ ENVIRONMENT=development
12
+ DEBUG=True
13
+ HOST=0.0.0.0
14
+ PORT=8000
15
+
16
+ # Database Configuration
17
+ DATABASE_URL=postgresql://shapetrack:shapetrack_password@localhost:5432/shapetrack
18
+ # For development, you can use SQLite:
19
+ # DATABASE_URL=sqlite:///./shapetrack.db
20
+
21
+ # Security & Encryption
22
+ SECRET_KEY=your-secret-key-here-change-in-production
23
+ ENCRYPTION_KEY=your-encryption-key-here-change-in-production
24
+ JWT_ALGORITHM=RS256
25
+ JWT_EXPIRATION_MINUTES=1440
26
+
27
+ # Storage Paths
28
+ BLOB_STORAGE_PATH=./storage/blobs
29
+ COMMIT_STORAGE_PATH=./storage/commits
30
+ REPOSITORY_STORAGE_PATH=./storage/repositories
31
+
32
+ # MissedTask Integration
33
+ MISSEDTASK_API_URL=http://localhost:3000
34
+ MISSEDTASK_API_KEY=your-missedtask-api-key
35
+
36
+ # CORS (for web UI)
37
+ ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
38
+
39
+ # Logging
40
+ LOG_LEVEL=INFO
41
+ LOG_FILE=./logs/shapetrack.log
42
+
43
+ # Admin User (created on first run)
44
+ # ADMIN_USERNAME is also the pinned "base admin": it is always kept as an admin on
45
+ # login and can never be demoted or deactivated via the API (lockout protection).
46
+ # In production this is set to "shappeadmin".
47
+ ADMIN_USERNAME=admin
48
+ ADMIN_EMAIL=admin@shappe.internal
49
+ ADMIN_PASSWORD=change-me-on-first-login
50
+
51
+ # ShappeTrack engine (Gitea) integration
52
+ AUTH_BACKEND=engine
53
+ GATEWAY_ENABLED=true
54
+ ENGINE_BASE_URL=http://127.0.0.1:3000
55
+ ENGINE_ADMIN_TOKEN=get-from-gitea-settings-applications
56
+ SHAPPETRACK_RUNNER_TOKEN=get-from-gitea-runner-registration
57
+
58
+ # ============================================================
59
+ # Public git access — shown to users in the repo "Clone" box.
60
+ # Dev defaults to localhost; in production set these to the real host.
61
+ # PUBLIC_GIT_HOST=track-api.shappe.ai
62
+ # PUBLIC_GIT_SCHEME=https
63
+ # SSH_HOST=track-api.shappe.ai
64
+ # SSH_PORT=22 # 22 if the NLB exposes 22 (clean shappetrack@host URLs)
65
+ # SSH_USER is the branded login user shown in git@ URLs (keep "shappetrack").
66
+ # ============================================================
67
+ PUBLIC_GIT_HOST=localhost:8000
68
+ PUBLIC_GIT_SCHEME=http
69
+ SSH_USER=shappetrack
70
+ SSH_HOST=localhost
71
+ SSH_PORT=2222
72
+
73
+ # Email / SMTP (password reset, notifications). Local dev leaves EMAIL_ENABLED=false
74
+ # (email is simply skipped). Production sets these — SMTP creds come from SSM/Secrets.
75
+ EMAIL_ENABLED=false
76
+ SMTP_HOST=
77
+ SMTP_PORT=587
78
+ SMTP_USE_TLS=true
79
+ FROM_EMAIL=track@shappemail.com
80
+ # Used to build links in emails (e.g. password-reset). Prod: https://track.shappe.ai
81
+ FRONTEND_BASE_URL=http://localhost:6173
82
+
83
+ # Migrations: prod runs alembic separately, so the container sets RUN_MIGRATIONS=false.
84
+ # Locally you can set true to auto-migrate on startup.
85
+ RUN_MIGRATIONS=true
86
+
87
+ # Git committer identity — the backend shells out to `git merge` (core/merge_service.py)
88
+ # to merge pull requests. git refuses to create a commit without an identity
89
+ # ("empty ident name"). These env vars are inherited by the git subprocess.
90
+ GIT_AUTHOR_NAME=ShappeTrack
91
+ GIT_AUTHOR_EMAIL=noreply@shappe.ai
92
+ GIT_COMMITTER_NAME=ShappeTrack
93
+ GIT_COMMITTER_EMAIL=noreply@shappe.ai
94
+
95
+ # ============================================================
96
+ # docker-compose infrastructure values (LOCAL DEV ONLY).
97
+ # In production (ECS) these come from AWS Secrets Manager / SSM / task env,
98
+ # NOT from this file. See docs/config-inventory.md.
99
+ # ============================================================
100
+ # Postgres (the postgres service + engine DB share these)
101
+ POSTGRES_USER=shape
102
+ POSTGRES_PASSWORD=Shappe@1234 # DEV ONLY — use a strong secret in prod
103
+ POSTGRES_DB=shape-track
104
+ # Gitea engine public host (compose). Prod: the engine's internal/public host.
105
+ ENGINE_DOMAIN=localhost
106
+ ENGINE_ROOT_URL=http://localhost:3000/
107
+ ENGINE_SSH_DOMAIN=localhost # host advertised in git@ SSH clone URLs
@@ -0,0 +1,312 @@
1
+ name: CI/CD — Backend
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: backend-${{ github.ref }}
14
+ cancel-in-progress: false # never cancel a deploy mid-flight
15
+
16
+ env:
17
+ AWS_REGION: ap-south-1
18
+ AWS_ROLE_ARN: arn:aws:iam::626635399249:role/shappetrack-backend-gh-deploy
19
+ ECR_REPO: 626635399249.dkr.ecr.ap-south-1.amazonaws.com/shappetrack-backend
20
+ ECS_CLUSTER: shappetrack-cluster
21
+ ECS_SERVICE: shappetrack-backend
22
+ CONTAINER_NAME: backend
23
+ MIGRATE_CONTAINER: migrate
24
+ MIGRATE_TASKDEF: shappetrack-migrate
25
+ MIGRATE_LOG_GROUP: /ecs/shappetrack-backend
26
+ HEALTH_URL: https://track-api.shappe.ai/health
27
+ PRIVATE_SUBNETS: subnet-0e4b373e2d2be0f0c,subnet-0fd7355f30a5c4f21
28
+ BACKEND_SG: sg-04fd5f7806cdb3287
29
+
30
+ jobs:
31
+ # ── JOB 1: security ────────────────────────────────────────────
32
+ security:
33
+ name: Secret Scan & SAST
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - name: Checkout (full history)
37
+ uses: actions/checkout@v4
38
+ with:
39
+ fetch-depth: 0
40
+
41
+ - name: Secret scan (TruffleHog — verified only)
42
+ uses: trufflesecurity/trufflehog@main
43
+ with:
44
+ path: ./
45
+ base: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
46
+ head: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.event.after }}
47
+ extra_args: --only-verified
48
+
49
+ # Dependency vulnerability scanning is handled by pip-audit (JOB 2).
50
+ # The GitHub dependency-review-action requires GitHub Advanced Security
51
+ # on private repos, so it is intentionally not used here.
52
+
53
+ - name: Semgrep SAST (Python)
54
+ uses: semgrep/semgrep-action@v1
55
+ with:
56
+ config: >-
57
+ p/security-audit
58
+ p/owasp-top-ten
59
+ p/python
60
+
61
+ # ── JOB 2: CI — lint, CVE, tests, MIGRATION TEST, docker build ─
62
+ ci:
63
+ name: Lint · CVE · Test · Migrate · Build
64
+ runs-on: ubuntu-latest
65
+ needs: security
66
+ # Throwaway Postgres for the migration test — spun up by GitHub, dies with the job.
67
+ # No AWS, no cost. Lets us run `alembic upgrade head` against a REAL Postgres so
68
+ # migration conflicts (e.g. DuplicateColumn) fail CI instead of reaching production.
69
+ services:
70
+ postgres:
71
+ image: postgres:15
72
+ env:
73
+ POSTGRES_USER: citest
74
+ POSTGRES_PASSWORD: citest
75
+ POSTGRES_DB: citestdb
76
+ ports:
77
+ - 5432:5432
78
+ options: >-
79
+ --health-cmd pg_isready
80
+ --health-interval 10s
81
+ --health-timeout 5s
82
+ --health-retries 5
83
+ steps:
84
+ - uses: actions/checkout@v4
85
+
86
+ - name: Setup Python
87
+ uses: actions/setup-python@v5
88
+ with:
89
+ python-version: "3.11"
90
+ cache: pip
91
+
92
+ - name: Install dev dependencies
93
+ run: pip install -r requirements-dev.txt
94
+
95
+ - name: Lint (ruff — non-blocking until debt cleared)
96
+ run: pip install ruff && ruff check . || true
97
+
98
+ - name: Dependency CVE scan (pip-audit — non-blocking)
99
+ run: pip install pip-audit && pip-audit -r requirements.txt || true
100
+
101
+ - name: Run tests (pytest — sqlite in-memory, blocking)
102
+ run: pytest -q
103
+
104
+ # ⭐ MIGRATION TEST — run the full alembic chain against a REAL Postgres.
105
+ # Catches DuplicateColumn / conflicting / non-idempotent migrations BEFORE
106
+ # they can reach production. upgrade head -> downgrade base -> upgrade head
107
+ # also proves migrations are reversible and idempotent. BLOCKING.
108
+ - name: Test migrations on real Postgres
109
+ env:
110
+ DATABASE_URL: postgresql://citest:citest@localhost:5432/citestdb
111
+ ENVIRONMENT: ci
112
+ SECRET_KEY: ci-dummy-secret-key-not-used-in-prod
113
+ ENCRYPTION_KEY: ci-dummy-encryption-key-not-used-in-prod
114
+ ADMIN_PASSWORD: ci-dummy-admin-pass
115
+ ALLOWED_ORIGINS: "*"
116
+ run: |
117
+ # Full round-trip on a clean Postgres (industry best practice):
118
+ # upgrade head -> catches conflicts / DuplicateColumn / missing-dep migrations
119
+ # downgrade base-> verifies every migration is reversible (incl. ENUM cleanup)
120
+ # upgrade head -> verifies idempotency (re-upgrade after full downgrade works)
121
+ # Any broken/non-reversible migration fails CI here, before it can reach prod.
122
+ alembic upgrade head
123
+ alembic downgrade base
124
+ alembic upgrade head
125
+
126
+ - name: Docker build (verify image builds — blocking)
127
+ run: docker build -t shappetrack-backend:ci .
128
+
129
+ # ── JOB 3: deploy (auto on push to main) ──────────────────────
130
+ deploy:
131
+ name: Deploy to ECS
132
+ runs-on: ubuntu-latest
133
+ needs: ci
134
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
135
+ permissions:
136
+ contents: read
137
+ id-token: write
138
+ steps:
139
+ - name: Checkout (full history — needed to diff the whole push range for migrations)
140
+ uses: actions/checkout@v4
141
+ with:
142
+ fetch-depth: 0
143
+
144
+ - name: Configure AWS credentials (OIDC)
145
+ uses: aws-actions/configure-aws-credentials@v4
146
+ with:
147
+ role-to-assume: ${{ env.AWS_ROLE_ARN }}
148
+ aws-region: ${{ env.AWS_REGION }}
149
+
150
+ - name: Login to ECR
151
+ uses: aws-actions/amazon-ecr-login@v2
152
+
153
+ # Immutable tag: git SHA + run attempt (unique even on re-runs)
154
+ - name: Build & push image
155
+ id: build
156
+ run: |
157
+ TAG="${{ github.sha }}-${{ github.run_attempt }}"
158
+ IMAGE="$ECR_REPO:$TAG"
159
+ docker build -t "$IMAGE" .
160
+ docker push "$IMAGE"
161
+ echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
162
+
163
+ # Record current task-def so we can roll back if the deploy goes bad
164
+ - name: Record rollback task-def
165
+ run: |
166
+ CUR=$(aws ecs describe-services --cluster "$ECS_CLUSTER" --services "$ECS_SERVICE" \
167
+ --query "services[0].taskDefinition" --output text)
168
+ echo "ROLLBACK_TASKDEF=$CUR" >> "$GITHUB_ENV"
169
+ echo "Rollback target: $CUR"
170
+
171
+ # ── Decide whether to run the migration release-phase task.
172
+ # LESSON (twice bitten): "HEAD~1..HEAD" only sees the LAST commit. When a
173
+ # push contains several commits and the migration is in an earlier one,
174
+ # that window misses it and the migration is silently skipped while the
175
+ # new app code already expects the new schema -> prod 500s.
176
+ # Fix: diff across the ENTIRE push range (github.event.before..sha). And if
177
+ # that range can't be computed for ANY reason, FAIL SAFE = run migrations.
178
+ # Running is safe even with no changes: `alembic upgrade head` is a no-op
179
+ # when the DB is already at head (verified from logs below). A needless
180
+ # ~20s migration task is far cheaper than a skipped-but-needed migration.
181
+ - name: Detect migration changes
182
+ id: migcheck
183
+ env:
184
+ BEFORE_SHA: ${{ github.event.before }}
185
+ run: |
186
+ RUN=false
187
+ # Zero SHA (first push to the branch) or unfetched base -> can't diff -> run.
188
+ if [ -z "$BEFORE_SHA" ] || [ "$BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
189
+ echo "No usable before-SHA — running migrations to be safe."
190
+ RUN=true
191
+ elif ! git cat-file -e "$BEFORE_SHA^{commit}" 2>/dev/null; then
192
+ # base commit not in the shallow checkout — deepen, else fail safe
193
+ git fetch --depth=50 origin "$BEFORE_SHA" 2>/dev/null || true
194
+ if ! git cat-file -e "$BEFORE_SHA^{commit}" 2>/dev/null; then
195
+ echo "before-SHA $BEFORE_SHA not fetchable — running migrations to be safe."
196
+ RUN=true
197
+ fi
198
+ fi
199
+ if [ "$RUN" != "true" ]; then
200
+ CHANGED=$(git diff --name-only "$BEFORE_SHA" "${{ github.sha }}" -- alembic/versions/ || echo "DIFF_FAILED")
201
+ if [ "$CHANGED" = "DIFF_FAILED" ]; then
202
+ echo "git diff failed — running migrations to be safe."
203
+ RUN=true
204
+ elif [ -n "$CHANGED" ]; then
205
+ echo "Migration files changed across the push:"; echo "$CHANGED"
206
+ RUN=true
207
+ else
208
+ echo "No migration changes across ${BEFORE_SHA}..${{ github.sha }} — skipping migration step."
209
+ fi
210
+ fi
211
+ echo "changed=$RUN" >> "$GITHUB_OUTPUT"
212
+
213
+ # ── RELEASE PHASE: run migration task, verify from LOGS (not just exit 0)
214
+ - name: Render migrate task-def
215
+ if: steps.migcheck.outputs.changed == 'true'
216
+ id: rendermig
217
+ uses: aws-actions/amazon-ecs-render-task-definition@v1
218
+ with:
219
+ task-definition-family: ${{ env.MIGRATE_TASKDEF }}
220
+ container-name: ${{ env.MIGRATE_CONTAINER }}
221
+ image: ${{ steps.build.outputs.image }}
222
+
223
+ - name: Run & verify migration
224
+ if: steps.migcheck.outputs.changed == 'true'
225
+ run: |
226
+ # The render action can emit "tags": [] which register-task-definition rejects
227
+ # ("Tags can not be empty"). Strip an empty tags array before registering.
228
+ jq 'if .tags == [] then del(.tags) else . end' ${{ steps.rendermig.outputs.task-definition }} > /tmp/migrate-td.json
229
+ ARN=$(aws ecs register-task-definition \
230
+ --cli-input-json file:///tmp/migrate-td.json \
231
+ --query "taskDefinition.taskDefinitionArn" --output text)
232
+ echo "Running migration: $ARN"
233
+ TASK=$(aws ecs run-task --cluster "$ECS_CLUSTER" --task-definition "$ARN" --launch-type EC2 \
234
+ --network-configuration "awsvpcConfiguration={subnets=[${PRIVATE_SUBNETS}],securityGroups=[${BACKEND_SG}]}" \
235
+ --query "tasks[0].taskArn" --output text)
236
+ aws ecs wait tasks-stopped --cluster "$ECS_CLUSTER" --tasks "$TASK"
237
+ EXIT=$(aws ecs describe-tasks --cluster "$ECS_CLUSTER" --tasks "$TASK" \
238
+ --query "tasks[0].containers[0].exitCode" --output text)
239
+ echo "Migration exit code: $EXIT"
240
+ # The container exit code is the AUTHORITATIVE pass/fail. `alembic upgrade`
241
+ # exits non-zero on any Python/DDL error, and Postgres transactional DDL
242
+ # rolls the whole upgrade back on failure — so exit 0 means the migration
243
+ # applied (or was already at head). A non-numeric/empty EXIT (couldn't read
244
+ # it) is also a hard fail.
245
+ case "$EXIT" in
246
+ 0) echo "Migration container exited 0 — success." ;;
247
+ *) echo "Migration FAILED (exit=$EXIT) — aborting"; exit 1 ;;
248
+ esac
249
+
250
+ # Fetch the logs for HUMAN VISIBILITY and a best-effort sanity check ONLY.
251
+ # A failure to READ the logs (IAM/timing) must NOT abort a migration that
252
+ # already exited 0 — that false-abort bit us once. We isolate the log text
253
+ # from any AWS-CLI error so the CLI's own "...Error..." can't be mistaken
254
+ # for a migration error.
255
+ TASK_ID=$(basename "$TASK")
256
+ sleep 5
257
+ if LOGS=$(aws logs get-log-events --log-group-name "$MIGRATE_LOG_GROUP" \
258
+ --log-stream-name "migrate/${MIGRATE_CONTAINER}/${TASK_ID}" \
259
+ --query "events[*].message" --output text 2>/dev/null); then
260
+ echo "Migration logs:"; echo "$LOGS"
261
+ # Only a genuine migration-side failure signature downgrades a 0-exit to
262
+ # a hard fail (belt-and-suspenders; should never fire when exit==0).
263
+ if echo "$LOGS" | grep -qiE "Traceback|SQLAlchemyError|psycopg|DuplicateColumn|UndefinedTable|relation .* does not exist|type .* already exists"; then
264
+ echo "Migration exited 0 but logs show a failure signature — aborting to be safe"; exit 1
265
+ fi
266
+ echo "Migration verified (exit 0 + logs clean)."
267
+ else
268
+ # Could not read logs — do NOT abort. Exit 0 already proved success.
269
+ echo "Note: could not read migration logs (permissions/timing); relying on exit code 0."
270
+ fi
271
+
272
+ # ── Deploy app: render committed task-def with new image, update service, wait stable
273
+ - name: Render backend task-def (committed file)
274
+ id: renderapp
275
+ uses: aws-actions/amazon-ecs-render-task-definition@v1
276
+ with:
277
+ task-definition: task-definition.json
278
+ container-name: ${{ env.CONTAINER_NAME }}
279
+ image: ${{ steps.build.outputs.image }}
280
+
281
+ - name: Deploy to ECS + wait stable
282
+ uses: aws-actions/amazon-ecs-deploy-task-definition@v2
283
+ with:
284
+ task-definition: ${{ steps.renderapp.outputs.task-definition }}
285
+ service: ${{ env.ECS_SERVICE }}
286
+ cluster: ${{ env.ECS_CLUSTER }}
287
+ wait-for-service-stability: true
288
+
289
+ # ── Smoke test the live API
290
+ - name: Smoke test /health
291
+ id: smoke
292
+ run: |
293
+ for i in 1 2 3 4 5 6; do
294
+ code=$(curl -s -o /tmp/h.json -w "%{http_code}" "$HEALTH_URL")
295
+ if [ "$code" = "200" ] && grep -q '"status"' /tmp/h.json; then
296
+ echo "Healthy (HTTP $code): $(cat /tmp/h.json)"; echo "ok=true" >> "$GITHUB_OUTPUT"; exit 0
297
+ fi
298
+ echo "Attempt $i: HTTP $code — retry 10s"; sleep 10
299
+ done
300
+ echo "ok=false" >> "$GITHUB_OUTPUT"
301
+
302
+ # ── Rollback to previous task-def on smoke failure
303
+ - name: Rollback on smoke failure
304
+ if: steps.smoke.outputs.ok == 'false'
305
+ run: |
306
+ echo "Smoke failed — rolling back to $ROLLBACK_TASKDEF"
307
+ aws ecs update-service --cluster "$ECS_CLUSTER" --service "$ECS_SERVICE" \
308
+ --task-definition "$ROLLBACK_TASKDEF" --force-new-deployment
309
+ aws ecs wait services-stable --cluster "$ECS_CLUSTER" --services "$ECS_SERVICE"
310
+ code=$(curl -s -o /dev/null -w "%{http_code}" "$HEALTH_URL")
311
+ echo "Post-rollback health: HTTP $code"
312
+ exit 1
@@ -0,0 +1,42 @@
1
+ # ===== Secrets (NEVER commit) =====
2
+ .env
3
+ .env.local
4
+ .env.*.local
5
+ docs/engine-credentials.local.md
6
+
7
+ # ===== Python =====
8
+ venv/
9
+ .venv/
10
+ env/
11
+ __pycache__/
12
+ *.py[cod]
13
+ *.pyo
14
+ *.egg-info/
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ # Python build artifacts (CLI packaging: wheels, sdists, PyInstaller output)
18
+ dist/
19
+ build/
20
+ *.spec
21
+ .ruff_cache/
22
+
23
+ # ===== Database / runtime data =====
24
+ *.db
25
+ *.sqlite
26
+ *.sqlite3
27
+ storage/
28
+ logs/
29
+ *.log
30
+
31
+ # ===== OS / editor =====
32
+ .DS_Store
33
+ Thumbs.db
34
+ .idea/
35
+ .vscode/*
36
+ !.vscode/extensions.json
37
+
38
+ # ===== Backups / scratch =====
39
+ *.backup
40
+ *.bak
41
+ *.orig
42
+ *.tmp
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,84 @@
1
+ # ShappeTrack Backend — production image
2
+ # Stack proven from repo: Python 3.11 + FastAPI, run by gunicorn/uvicorn worker.
3
+ # (requirements.txt, main.py, docker-entrypoint.sh)
4
+
5
+ # ---------- Stage 1: build dependency wheels ----------
6
+ # Same base as runtime so wheels are ABI-compatible.
7
+ FROM python:3.11-slim AS builder
8
+
9
+ WORKDIR /app
10
+
11
+ # No compiler installed: every dependency in requirements.txt is pure-Python or
12
+ # ships a manylinux wheel (psycopg2-BINARY, cryptography, bcrypt). Verified — no
13
+ # source builds are required.
14
+ COPY requirements.txt .
15
+
16
+ # Build wheels for all pinned deps into /wheels for a clean copy into runtime.
17
+ # requirements.txt is fully version-pinned (no lockfile in repo) -> reproducible.
18
+ RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
19
+
20
+
21
+ # ---------- Stage 2: runtime ----------
22
+ FROM python:3.11-slim AS runtime
23
+
24
+ # PYTHONUNBUFFERED — flush stdout/stderr immediately so logs reach
25
+ # CloudWatch in real time and aren't lost on crash.
26
+ # PYTHONDONTWRITEBYTECODE — no .pyc clutter in an immutable/ephemeral container.
27
+ ENV PYTHONUNBUFFERED=1 \
28
+ PYTHONDONTWRITEBYTECODE=1
29
+
30
+ # Runtime OS deps:
31
+ # git — the app shells out to the git binary via subprocess
32
+ # (core/git_server.py: "git init --bare", "git log", "git for-each-ref";
33
+ # core/merge_service.py: "git rev-parse", "git merge-base").
34
+ # tini — PID 1 init that forwards signals (SIGTERM from ECS) and REAPS the
35
+ # orphaned git subprocesses the app spawns, preventing zombie buildup.
36
+ # postgresql-client and gcc are intentionally NOT installed — nothing in the
37
+ # repo invokes psql/pg_* and no dependency compiles from source.
38
+ RUN apt-get update \
39
+ && apt-get install -y --no-install-recommends git tini \
40
+ && rm -rf /var/lib/apt/lists/*
41
+
42
+ WORKDIR /app
43
+
44
+ # Install the prebuilt wheels, then drop them. --no-cache-dir keeps pip's cache
45
+ # out of the final layer.
46
+ COPY --from=builder /wheels /wheels
47
+ COPY requirements.txt .
48
+ RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt \
49
+ && rm -rf /wheels
50
+
51
+ # Application source. .dockerignore excludes .env*, storage/, logs/, .git, caches
52
+ # and tests/ so secrets and runtime state never enter the image.
53
+ COPY . .
54
+
55
+ # Entrypoint is a real tracked script (docker-entrypoint.sh): it runs
56
+ # `alembic upgrade head` (gated by RUN_MIGRATIONS) then execs gunicorn.
57
+ RUN chmod +x /app/docker-entrypoint.sh
58
+
59
+ # Writable runtime directories, owned by the non-root user:
60
+ # storage/ — bare git repos (config.py GIT_STORAGE_PATH=./storage/git) and
61
+ # release uploads (api/v1/releases.py writes BLOB_STORAGE_PATH/releases/<id>)
62
+ # logs/ — config.py ensure_storage_paths() creates dirname(LOG_FILE)=./logs
63
+ # (Application logs go to stdout, but the logs/ dir is still created on boot.)
64
+ RUN useradd --create-home --uid 10001 appuser \
65
+ && mkdir -p /app/storage /app/logs \
66
+ && chown -R appuser:appuser /app/storage /app/logs
67
+
68
+ USER appuser
69
+
70
+ # Port 8000 is the only bound port (config.py HOST=0.0.0.0 PORT=8000; entrypoint
71
+ # binds ${HOST}:${PORT}). No SSH listener exists in this process, so 2222 is NOT
72
+ # exposed (SSH_PORT in config.py is advertised clone-URL metadata only).
73
+ EXPOSE 8000
74
+
75
+ # /health is a real unauthenticated endpoint (main.py @app.get("/health")).
76
+ # Use the stdlib so the check needs no extra packages.
77
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
78
+ CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health', timeout=5).status==200 else 1)" || exit 1
79
+
80
+ # tini as PID 1 (exec form): forwards SIGTERM to the entrypoint and reaps the
81
+ # orphaned git subprocesses the app spawns. The entrypoint then `exec`s gunicorn
82
+ # so signals reach it directly.
83
+ # Reads ENVIRONMENT / WEB_CONCURRENCY / GUNICORN_TIMEOUT / RUN_MIGRATIONS from env.
84
+ ENTRYPOINT ["/usr/bin/tini", "--", "/app/docker-entrypoint.sh"]
@@ -0,0 +1,84 @@
1
+ Metadata-Version: 2.4
2
+ Name: shappetrack
3
+ Version: 1.0.0
4
+ Summary: ShappeTrack CLI — clone, commit, and push ShappeTrack repositories with one app token. The git engine stays invisible.
5
+ Project-URL: Homepage, https://track.shappe.ai
6
+ Author: ShappeTrack
7
+ License: Proprietary
8
+ Keywords: cli,git,shappetrack,vcs
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Version Control :: Git
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: httpx==0.26.0
15
+ Requires-Dist: typer<1.0,>=0.12
16
+ Description-Content-Type: text/markdown
17
+
18
+ # ShappeTrack CLI
19
+
20
+ Clone, commit, and push ShappeTrack repositories with **one app token**. Real
21
+ `git` runs under the hood; the git engine is never exposed.
22
+
23
+ ```
24
+ shappetrack login --server https://track-api.shappe.ai/api/v1
25
+ shappetrack clone my-repo
26
+ ```
27
+
28
+ ## Install
29
+
30
+ The CLI is a small Python package (`shappetrack-cli`). The recommended way is
31
+ [`pipx`](https://pipx.pypa.io/), which installs it in an isolated environment so
32
+ it never clashes with your other Python tools.
33
+
34
+ ### For end users (once published to PyPI)
35
+
36
+ ```bash
37
+ pipx install shappetrack
38
+ ```
39
+
40
+ ### From source (internal / development)
41
+
42
+ From the directory containing `pyproject.toml`:
43
+
44
+ ```bash
45
+ pipx install . # isolated, recommended
46
+ # or
47
+ pip install . # into the current environment
48
+ ```
49
+
50
+ Requires **Python 3.9+** and **git** on your PATH (the CLI runs real git under the hood).
51
+
52
+ ## Usage
53
+
54
+ 1. Create an app token in the web app: **Settings → Access Tokens** (starts with `shp_`).
55
+ 2. Log in once — this configures git credentials automatically:
56
+
57
+ ```bash
58
+ shappetrack login --server https://track-api.shappe.ai/api/v1
59
+ ```
60
+
61
+ 3. Then use the familiar commands (they run standard git underneath):
62
+
63
+ ```bash
64
+ shappetrack clone my-repo
65
+ shappetrack status
66
+ shappetrack add .
67
+ shappetrack commit -m "message" --task MT-101
68
+ shappetrack push
69
+ shappetrack pull
70
+ shappetrack branch feature-x
71
+ ```
72
+
73
+ ## How it works
74
+
75
+ `shappetrack clone my-repo` runs `git clone https://track-api.shappe.ai/repos/my-repo`
76
+ using the app token you logged in with. The backend proxies git traffic to the
77
+ internal engine — you never see or handle the engine, its host, or its token.
78
+
79
+ You can also clone with plain git directly (no CLI needed):
80
+
81
+ ```bash
82
+ git clone https://track-api.shappe.ai/repos/my-repo
83
+ # username: your ShappeTrack username, password: your shp_ app token
84
+ ```