Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ dist
projects/ui
scripts
docs
deploy
1 change: 1 addition & 0 deletions .gcloudignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ dist
projects/ui
scripts
docs
deploy
1 change: 1 addition & 0 deletions .github/workflows/cloud-run-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
source: .
env_vars: |
NODE_ENV=production
PARTY_ENABLED=${{ vars.PARTY_ENABLED || 'false' }}
secrets: |
DATABASE_URL=DATABASE_URL:latest
SPOTIFY_CLIENT_ID=SPOTIFY_CLIENT_ID:latest
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/party-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Party checks
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run build
- run: npm test --workspace=@brewtify/spotify && npm test --workspace=api && npm run test:integration --workspace=api
- run: npm run lint:party --workspace=mini-app && npm test --workspace=mini-app
12 changes: 10 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,26 @@ COPY projects/api/prisma ./projects/api/prisma/
COPY projects/api/prisma.config.ts ./projects/api/prisma.config.ts
COPY projects/shared/package.json projects/shared/tsconfig.json ./projects/shared/
COPY projects/shared/src ./projects/shared/src/
COPY projects/spotify/package.json projects/spotify/tsconfig.json ./projects/spotify/
COPY projects/spotify/src ./projects/spotify/src/
COPY projects/tap/package.json projects/tap/tsconfig.json ./projects/tap/
COPY projects/tap/src ./projects/tap/src/
COPY projects/mini-app/package.json projects/mini-app/tsconfig.json projects/mini-app/tsconfig.app.json projects/mini-app/tsconfig.node.json ./projects/mini-app/
COPY projects/mini-app/index.html projects/mini-app/vite.config.ts ./projects/mini-app/
COPY projects/mini-app/src ./projects/mini-app/src/
COPY projects/mini-app/public ./projects/mini-app/public/
COPY projects/mini-app/dev ./projects/mini-app/dev/

# Install all dependencies (including devDependencies for build)
RUN npm ci --workspace=projects/api --workspace=projects/shared --workspace=projects/tap --workspace=projects/mini-app --include-workspace-root
RUN npm ci --workspace=projects/api --workspace=projects/shared --workspace=projects/spotify --workspace=projects/tap --workspace=projects/mini-app --include-workspace-root

# Build shared package
WORKDIR /app/projects/shared
RUN npx tsc

WORKDIR /app/projects/spotify
RUN npx tsc

# Build tap package
WORKDIR /app/projects/tap
RUN npx tsc
Expand All @@ -49,12 +55,14 @@ WORKDIR /app
COPY package.json package-lock.json ./
COPY projects/api/package.json ./projects/api/
COPY projects/shared/package.json ./projects/shared/
COPY projects/spotify/package.json ./projects/spotify/
COPY projects/tap/package.json ./projects/tap/
COPY --from=builder /app/projects/shared/dist ./projects/shared/dist/
COPY --from=builder /app/projects/spotify/dist ./projects/spotify/dist/
COPY --from=builder /app/projects/tap/dist ./projects/tap/dist/

# Install production dependencies only
RUN npm ci --workspace=projects/api --workspace=projects/shared --workspace=projects/tap --include-workspace-root --omit=dev
RUN npm ci --workspace=projects/api --workspace=projects/shared --workspace=projects/spotify --workspace=projects/tap --include-workspace-root --omit=dev

# Copy built output (includes generated Prisma client in dist/generated/)
COPY --from=builder /app/projects/api/dist ./projects/api/dist/
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
# Brewtify
Playlists Brewery for Spotify

## Party Queue pilot

The existing Telegram Mini App includes a feature-gated cross-service Party Queue.
See [setup, privacy, deployment and release gates](docs/party-queue.md) before enabling
`PARTY_ENABLED`. Library credentials and playback-only Party authorization are separate.
6 changes: 6 additions & 0 deletions deploy/party/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.terraform/
*.tfstate
*.tfstate.*
*.tfvars
*.tfplan
crash.log
22 changes: 22 additions & 0 deletions deploy/party/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 127 additions & 0 deletions deploy/party/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
terraform {
required_version = ">= 1.6"
required_providers {
google = { source = "hashicorp/google", version = "~> 6.0" }
}
}

variable "project_id" { type = string }
variable "region" { type = string }
variable "service_url" { type = string }
variable "runtime_service_account" { type = string }
variable "cloud_run_service" { type = string }
variable "scheduler_region" {
type = string
description = "Cloud Scheduler-supported region; need not match Cloud Run."
}

provider "google" {
project = var.project_id
region = var.region
}
data "google_project" "current" {}

resource "google_project_service" "tasks" {
service = "cloudtasks.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "scheduler" {
service = "cloudscheduler.googleapis.com"
disable_on_destroy = false
}
resource "google_service_account" "party_tasks" {
account_id = "brewtify-party-tasks"
display_name = "Brewtify Party internal task identity"
}
resource "google_cloud_tasks_queue" "party" {
name = "brewtify-party"
location = var.region
depends_on = [google_project_service.tasks]
rate_limits {
max_concurrent_dispatches = 5
max_dispatches_per_second = 5
}
retry_config {
max_attempts = 8
min_backoff = "5s"
max_backoff = "120s"
max_doublings = 4
max_retry_duration = "1800s"
}
}
resource "google_cloud_tasks_queue_iam_member" "enqueue" {
project = var.project_id
location = var.region
name = google_cloud_tasks_queue.party.name
role = "roles/cloudtasks.enqueuer"
member = "serviceAccount:${var.runtime_service_account}"
}
resource "google_service_account_iam_member" "act_as" {
service_account_id = google_service_account.party_tasks.name
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${var.runtime_service_account}"
}
resource "google_service_account_iam_member" "tasks_token" {
service_account_id = google_service_account.party_tasks.name
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:service-${data.google_project.current.number}@gcp-sa-cloudtasks.iam.gserviceaccount.com"
depends_on = [google_project_service.tasks]
}
resource "google_service_account_iam_member" "scheduler_token" {
service_account_id = google_service_account.party_tasks.name
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:service-${data.google_project.current.number}@gcp-sa-cloudscheduler.iam.gserviceaccount.com"
depends_on = [google_project_service.scheduler]
}
resource "google_cloud_run_service_iam_member" "invoker" {
project = var.project_id
location = var.region
service = var.cloud_run_service
role = "roles/run.invoker"
member = "serviceAccount:${google_service_account.party_tasks.email}"
}
resource "google_cloud_scheduler_job" "maintenance" {
name = "brewtify-party-maintenance"
region = var.scheduler_region
schedule = "* * * * *"
time_zone = "Etc/UTC"
paused = true
depends_on = [google_project_service.scheduler]
attempt_deadline = "180s"
retry_config {
retry_count = 3
min_backoff_duration = "5s"
max_backoff_duration = "60s"
}
http_target {
uri = "${var.service_url}/internal/party/maintenance"
http_method = "POST"
headers = { "Content-Type" = "application/json" }
body = base64encode("{}")
oidc_token {
service_account_email = google_service_account.party_tasks.email
audience = var.service_url
}
}
}

# Cloud Run's request log otherwise stores full callback and launch query strings.
# Audit any additional log sinks/proxies separately before enabling real traffic.
resource "google_logging_project_exclusion" "party_capabilities" {
name = "brewtify-party-capability-urls"
description = "Do not retain OAuth or Telegram launch capabilities in request URLs."
filter = "resource.type=\"cloud_run_revision\" AND resource.labels.service_name=\"${var.cloud_run_service}\" AND (httpRequest.requestUrl:\"/api/party/auth/\" OR httpRequest.requestUrl:\"/app?\")"
}

output "party_environment" {
value = {
PARTY_ENABLED = "false"
PARTY_PUBLIC_ORIGIN = var.service_url
PARTY_SPOTIFY_REDIRECT_URI = "${var.service_url}/api/party/auth/callback"
PARTY_TASKS_PROJECT = var.project_id
PARTY_TASKS_LOCATION = var.region
PARTY_TASKS_QUEUE = google_cloud_tasks_queue.party.name
PARTY_TASKS_SERVICE_ACCOUNT = google_service_account.party_tasks.email
PARTY_TASKS_AUDIENCE = var.service_url
}
}
Loading
Loading