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.
- shappetrack-1.0.0/.dockerignore +48 -0
- shappetrack-1.0.0/.env.example +107 -0
- shappetrack-1.0.0/.github/workflows/deploy.yml +312 -0
- shappetrack-1.0.0/.gitignore +42 -0
- shappetrack-1.0.0/.python-version +1 -0
- shappetrack-1.0.0/Dockerfile +84 -0
- shappetrack-1.0.0/PKG-INFO +84 -0
- shappetrack-1.0.0/README.md +1469 -0
- shappetrack-1.0.0/__init__.py +0 -0
- shappetrack-1.0.0/alembic/README +1 -0
- shappetrack-1.0.0/alembic/env.py +79 -0
- shappetrack-1.0.0/alembic/script.py.mako +26 -0
- shappetrack-1.0.0/alembic/versions/05d4ee5e7fb1_initial_schema_all_tables.py +950 -0
- shappetrack-1.0.0/alembic/versions/a1b2c3d4e5f6_issue_pin_lock_due_date_time_entries.py +76 -0
- shappetrack-1.0.0/alembic/versions/b2c3d4e5f6a7_pr_pin_lock_due_date_time_entries.py +73 -0
- shappetrack-1.0.0/alembic/versions/c3d4e5f6a7b8_add_repo_extended_fields.py +36 -0
- shappetrack-1.0.0/alembic/versions/d1e2f3a4b5c6_add_projects_and_notification_prefs.py +79 -0
- shappetrack-1.0.0/alembic/versions/e2f3a4b5c6d7_avatar_url_to_text.py +28 -0
- shappetrack-1.0.0/alembic/versions/f3a4b5c6d7e8_add_chat_message_is_pinned.py +30 -0
- shappetrack-1.0.0/alembic/versions/g4b5c6d7e8f9_add_sprints_and_issue_sprint_fields.py +63 -0
- shappetrack-1.0.0/alembic/versions/h5c6d7e8f9a0_add_sla_configs.py +40 -0
- shappetrack-1.0.0/alembic/versions/i6d7e8f9a0b1_reset_tokens_and_issue_uniq.py +59 -0
- shappetrack-1.0.0/alembic/versions/j7e8f9a0b1c2_orphan_tables_runners_oauth_votes_mergequeue.py +99 -0
- shappetrack-1.0.0/alembic/versions/k8f9a0b1c2d3_webauthn_credentials_and_challenges.py +63 -0
- shappetrack-1.0.0/alembic.ini +116 -0
- shappetrack-1.0.0/api/__init__.py +3 -0
- shappetrack-1.0.0/api/v1/__init__.py +143 -0
- shappetrack-1.0.0/api/v1/admin.py +476 -0
- shappetrack-1.0.0/api/v1/analytics.py +425 -0
- shappetrack-1.0.0/api/v1/auth.py +629 -0
- shappetrack-1.0.0/api/v1/branch_protection.py +343 -0
- shappetrack-1.0.0/api/v1/branches.py +220 -0
- shappetrack-1.0.0/api/v1/changelog.py +139 -0
- shappetrack-1.0.0/api/v1/chat.py +527 -0
- shappetrack-1.0.0/api/v1/cli.py +46 -0
- shappetrack-1.0.0/api/v1/collaborators.py +362 -0
- shappetrack-1.0.0/api/v1/commits.py +331 -0
- shappetrack-1.0.0/api/v1/diffs.py +433 -0
- shappetrack-1.0.0/api/v1/feedback.py +103 -0
- shappetrack-1.0.0/api/v1/files.py +840 -0
- shappetrack-1.0.0/api/v1/gateway/__init__.py +1 -0
- shappetrack-1.0.0/api/v1/gateway/actions_settings.py +262 -0
- shappetrack-1.0.0/api/v1/gateway/avatars.py +42 -0
- shappetrack-1.0.0/api/v1/gateway/deps.py +37 -0
- shappetrack-1.0.0/api/v1/gateway/diffs.py +144 -0
- shappetrack-1.0.0/api/v1/gateway/explore.py +98 -0
- shappetrack-1.0.0/api/v1/gateway/extras.py +324 -0
- shappetrack-1.0.0/api/v1/gateway/issues.py +658 -0
- shappetrack-1.0.0/api/v1/gateway/milestones.py +669 -0
- shappetrack-1.0.0/api/v1/gateway/organizations.py +705 -0
- shappetrack-1.0.0/api/v1/gateway/pulls.py +515 -0
- shappetrack-1.0.0/api/v1/gateway/repo_admin.py +639 -0
- shappetrack-1.0.0/api/v1/gateway/repositories.py +1018 -0
- shappetrack-1.0.0/api/v1/gateway/schemas.py +362 -0
- shappetrack-1.0.0/api/v1/gateway/search.py +337 -0
- shappetrack-1.0.0/api/v1/gateway/translate.py +266 -0
- shappetrack-1.0.0/api/v1/gateway/users.py +615 -0
- shappetrack-1.0.0/api/v1/gateway/wiki.py +207 -0
- shappetrack-1.0.0/api/v1/gateway/workflows.py +664 -0
- shappetrack-1.0.0/api/v1/git_operations.py +199 -0
- shappetrack-1.0.0/api/v1/git_proxy.py +257 -0
- shappetrack-1.0.0/api/v1/issues.py +1075 -0
- shappetrack-1.0.0/api/v1/merge_queue.py +120 -0
- shappetrack-1.0.0/api/v1/milestones.py +514 -0
- shappetrack-1.0.0/api/v1/missedtask.py +466 -0
- shappetrack-1.0.0/api/v1/notifications.py +181 -0
- shappetrack-1.0.0/api/v1/organizations.py +605 -0
- shappetrack-1.0.0/api/v1/projects.py +296 -0
- shappetrack-1.0.0/api/v1/pull_requests.py +1042 -0
- shappetrack-1.0.0/api/v1/releases.py +552 -0
- shappetrack-1.0.0/api/v1/repositories.py +533 -0
- shappetrack-1.0.0/api/v1/safety.py +263 -0
- shappetrack-1.0.0/api/v1/search.py +646 -0
- shappetrack-1.0.0/api/v1/sla.py +158 -0
- shappetrack-1.0.0/api/v1/sprints.py +228 -0
- shappetrack-1.0.0/api/v1/ssh_keys.py +518 -0
- shappetrack-1.0.0/api/v1/stars.py +260 -0
- shappetrack-1.0.0/api/v1/teams.py +953 -0
- shappetrack-1.0.0/api/v1/tokens.py +110 -0
- shappetrack-1.0.0/api/v1/totp_utils.py +19 -0
- shappetrack-1.0.0/api/v1/two_factor.py +638 -0
- shappetrack-1.0.0/api/v1/user_follows.py +253 -0
- shappetrack-1.0.0/api/v1/users.py +929 -0
- shappetrack-1.0.0/api/v1/vcs.py +242 -0
- shappetrack-1.0.0/api/v1/votes.py +106 -0
- shappetrack-1.0.0/api/v1/watch.py +341 -0
- shappetrack-1.0.0/api/v1/webhooks.py +366 -0
- shappetrack-1.0.0/api/v1/workflows.py +500 -0
- shappetrack-1.0.0/auth/__init__.py +14 -0
- shappetrack-1.0.0/auth/engine_client.py +131 -0
- shappetrack-1.0.0/auth/jwt_handler.py +119 -0
- shappetrack-1.0.0/auth/password.py +45 -0
- shappetrack-1.0.0/auth/permissions.py +150 -0
- shappetrack-1.0.0/auth/principal.py +92 -0
- shappetrack-1.0.0/auth/scopes.py +54 -0
- shappetrack-1.0.0/auth/token_utils.py +50 -0
- shappetrack-1.0.0/config.py +200 -0
- shappetrack-1.0.0/core/__init__.py +13 -0
- shappetrack-1.0.0/core/access.py +41 -0
- shappetrack-1.0.0/core/audit.py +27 -0
- shappetrack-1.0.0/core/bootstrap.py +64 -0
- shappetrack-1.0.0/core/chat_events.py +78 -0
- shappetrack-1.0.0/core/chat_service.py +215 -0
- shappetrack-1.0.0/core/chat_ws.py +53 -0
- shappetrack-1.0.0/core/diff.py +238 -0
- shappetrack-1.0.0/core/email_service.py +70 -0
- shappetrack-1.0.0/core/encryption.py +150 -0
- shappetrack-1.0.0/core/git_server.py +332 -0
- shappetrack-1.0.0/core/logging.py +280 -0
- shappetrack-1.0.0/core/merge_service.py +486 -0
- shappetrack-1.0.0/core/notification_service.py +406 -0
- shappetrack-1.0.0/core/rate_limit.py +5 -0
- shappetrack-1.0.0/core/safety_guards.py +338 -0
- shappetrack-1.0.0/core/vcs.py +259 -0
- shappetrack-1.0.0/database.py +71 -0
- shappetrack-1.0.0/docker-compose.yml +144 -0
- shappetrack-1.0.0/docker-entrypoint.sh +49 -0
- shappetrack-1.0.0/docs/ci-runner-security.md +66 -0
- shappetrack-1.0.0/docs/config-inventory.md +92 -0
- shappetrack-1.0.0/docs/ssh-git-deployment.md +94 -0
- shappetrack-1.0.0/main.py +169 -0
- shappetrack-1.0.0/migrate_chat.py +45 -0
- shappetrack-1.0.0/migrate_to_engine.py +304 -0
- shappetrack-1.0.0/models/__init__.py +136 -0
- shappetrack-1.0.0/models/audit_log.py +55 -0
- shappetrack-1.0.0/models/branch.py +43 -0
- shappetrack-1.0.0/models/branch_protection.py +96 -0
- shappetrack-1.0.0/models/chat_channel.py +25 -0
- shappetrack-1.0.0/models/chat_member.py +17 -0
- shappetrack-1.0.0/models/chat_message.py +38 -0
- shappetrack-1.0.0/models/chat_reaction.py +15 -0
- shappetrack-1.0.0/models/commit.py +66 -0
- shappetrack-1.0.0/models/file_blob.py +47 -0
- shappetrack-1.0.0/models/issue.py +82 -0
- shappetrack-1.0.0/models/issue_comment.py +33 -0
- shappetrack-1.0.0/models/issue_reaction.py +35 -0
- shappetrack-1.0.0/models/issue_time_entry.py +26 -0
- shappetrack-1.0.0/models/issue_vote.py +16 -0
- shappetrack-1.0.0/models/merge_queue_entry.py +18 -0
- shappetrack-1.0.0/models/milestone.py +77 -0
- shappetrack-1.0.0/models/notification.py +125 -0
- shappetrack-1.0.0/models/oauth_application.py +47 -0
- shappetrack-1.0.0/models/organization.py +58 -0
- shappetrack-1.0.0/models/organization_member.py +66 -0
- shappetrack-1.0.0/models/password_reset_token.py +21 -0
- shappetrack-1.0.0/models/personal_access_token.py +57 -0
- shappetrack-1.0.0/models/pr_comment.py +39 -0
- shappetrack-1.0.0/models/pr_time_entry.py +22 -0
- shappetrack-1.0.0/models/project_board.py +27 -0
- shappetrack-1.0.0/models/project_card.py +22 -0
- shappetrack-1.0.0/models/project_column.py +27 -0
- shappetrack-1.0.0/models/pull_request.py +85 -0
- shappetrack-1.0.0/models/release.py +121 -0
- shappetrack-1.0.0/models/repo_meta.py +17 -0
- shappetrack-1.0.0/models/repo_notification_pref.py +30 -0
- shappetrack-1.0.0/models/repository.py +114 -0
- shappetrack-1.0.0/models/repository_collaborator.py +122 -0
- shappetrack-1.0.0/models/repository_label.py +30 -0
- shappetrack-1.0.0/models/repository_star.py +31 -0
- shappetrack-1.0.0/models/repository_watch.py +35 -0
- shappetrack-1.0.0/models/runner.py +58 -0
- shappetrack-1.0.0/models/session.py +46 -0
- shappetrack-1.0.0/models/sla_config.py +17 -0
- shappetrack-1.0.0/models/sprint.py +27 -0
- shappetrack-1.0.0/models/sprint_report.py +17 -0
- shappetrack-1.0.0/models/ssh_key.py +52 -0
- shappetrack-1.0.0/models/tag.py +49 -0
- shappetrack-1.0.0/models/team.py +52 -0
- shappetrack-1.0.0/models/team_member.py +44 -0
- shappetrack-1.0.0/models/team_repository_permission.py +47 -0
- shappetrack-1.0.0/models/two_factor_auth.py +34 -0
- shappetrack-1.0.0/models/user.py +143 -0
- shappetrack-1.0.0/models/user_follow.py +32 -0
- shappetrack-1.0.0/models/webauthn_challenge.py +25 -0
- shappetrack-1.0.0/models/webauthn_credential.py +45 -0
- shappetrack-1.0.0/models/webhook.py +45 -0
- shappetrack-1.0.0/models/workflow.py +54 -0
- shappetrack-1.0.0/models/workflow_job.py +65 -0
- shappetrack-1.0.0/models/workflow_run.py +67 -0
- shappetrack-1.0.0/models/workflow_step.py +66 -0
- shappetrack-1.0.0/pyproject.toml +40 -0
- shappetrack-1.0.0/pytest.ini +12 -0
- shappetrack-1.0.0/requirements-dev.txt +8 -0
- shappetrack-1.0.0/requirements.txt +55 -0
- shappetrack-1.0.0/scripts/create-engine-db.sql +5 -0
- shappetrack-1.0.0/scripts/generate_secret.py +5 -0
- shappetrack-1.0.0/scripts/make_admin.py +50 -0
- shappetrack-1.0.0/scripts/migrations/add_engine_columns.sql +4 -0
- shappetrack-1.0.0/scripts/migrations/add_repo_meta.sql +11 -0
- shappetrack-1.0.0/seed_data.py +117 -0
- shappetrack-1.0.0/shappetrack/README.md +67 -0
- shappetrack-1.0.0/shappetrack/RELEASING.md +89 -0
- shappetrack-1.0.0/shappetrack/__init__.py +7 -0
- shappetrack-1.0.0/shappetrack/__main__.py +4 -0
- shappetrack-1.0.0/shappetrack/__pyinstaller_entry.py +10 -0
- shappetrack-1.0.0/shappetrack/local.py +121 -0
- shappetrack-1.0.0/shappetrack/shapetrack.py +266 -0
- shappetrack-1.0.0/task-definition.json +207 -0
- shappetrack-1.0.0/tests/__init__.py +1 -0
- shappetrack-1.0.0/tests/conftest.py +74 -0
- shappetrack-1.0.0/tests/test_chat.py +43 -0
- shappetrack-1.0.0/tests/test_principal.py +65 -0
- shappetrack-1.0.0/tests/test_scopes.py +31 -0
- shappetrack-1.0.0/tests/test_secrets.py +54 -0
- shappetrack-1.0.0/tests/test_sessions.py +40 -0
- shappetrack-1.0.0/tests/test_token_utils.py +43 -0
- shappetrack-1.0.0/tests/test_tokens_api.py +57 -0
- 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
|
+
```
|