Files
thamanyah/CLAUDE.md
T
FahdShalhoub 96cabc5716
Build, Push and Deploy CMS / build-push-deploy (push) Successful in 2m17s
FEAT: Added Database To CMS Serbove
2026-08-16 22:19:03 +03:00

7.7 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Repo layout

.
├── cms/            Go service: video ingestion/CMS (implemented)
├── discovery/      Go service: empty placeholder, no code yet
├── infrastructure/ Pulumi (Go) program provisioning all AWS resources
└── .gitea/workflows/  Gitea Actions CI/CD pipelines

discovery is provisioned in infra (its own ECR repo, ECS service, ALB, Postgres database/role) but has no application code yet — don't assume it's deployable.

Commands

cms (Go 1.25, module thamanyah/cms/v2)

cd cms
go run .                    # serves on :8081
go build ./...
go vet ./...

There are no test files in this repo (cms, discovery, or infrastructure) — don't assume a test suite exists.

Views are written as .templ files (github.com/a-h/templ) and compiled to *_templ.go. If you edit a .templ file, regenerate its Go code with the templ generate CLI before building (not installed in this environment by default — install via go install github.com/a-h/templ/cmd/templ@v0.3.1020 to match go.mod, or check for an existing binary first).

infrastructure (Go, Pulumi, module thamanyah)

cd infrastructure
pulumi preview               # plan changes against stack "main"
pulumi up                    # apply — this touches real AWS resources, confirm with the user first
pulumi stack output          # e.g. ecsClusterArn, cmsServiceArn

Deploys run in CI (.gitea/workflows/infrastructure-deploy.yml) on push to main touching infrastructure/**. Treat local pulumi up as something to confirm with the user, not a routine dev command — it mutates shared cloud state and Pulumi state isn't safe to update concurrently with CI.

Architecture

cms service

Plain net/http (Go 1.22+ pattern-based ServeMux), no framework. Entry point cms/main.go wires up AWS SDK v2 clients (S3, MediaConvert) from env vars and assigns them to package-level interface vars in internal/services (services.S3Client, services.MediaConvertClient) — handlers call through these interfaces, and S3Concrete/MediaConvertConcrete are the only implementations, which is what makes the handlers testable even though no tests exist yet. On boot, S3Concrete.AssertSuccessfulConnection proactively exercises head/put/get/presign against the bucket and panics on failure, rather than letting the service come up in a broken state.

Routes (cms/main.go): GET /, GET /health, GET /videos/new, POST /videos/presign, POST /videos, static files under /static/.

Views live in cms/internal/views (templ components) with a shared layouts.Layout wrapper; types.go holds view-model structs like VideoMetadata used by the upload-success page.

Video upload → transcode pipeline

  1. Browser calls POST /videos/presign → cms returns a presigned S3 PUT URL for raw-uploads-bucket, key videos/<random-hex>.<ext>.
  2. Browser PUTs the file directly to S3 (requires the bucket's CORS rule, set up in infrastructure/main.go).
  3. Browser calls POST /videos with the metadata + key → cms calls MediaConvertClient.QueueEncodingJob(key), submitting a MediaConvert job s3://raw-uploads-bucket/<key>s3://encoded-bucket/<key> (H.264/AAC → MP4, QVBR rate control — QVBR requires MaxBitrate to be set explicitly).
  4. Finished output lands in encoded-bucket, served via CloudFront.

Config wiring: cms reads S3_BUCKET, MEDIACONVERT_INPUT_BUCKET, MEDIACONVERT_OUTPUT_BUCKET, MEDIACONVERT_ROLE_ARN, AWS_REGION from env vars injected by the ECS task definition (extraEnv in deployFargateService, infrastructure/main.go).

infrastructure (infrastructure/main.go, single Pulumi Go program, region us-east-1, stack main)

  • Postgres: one shared RDS instance (db.t3.micro, single-AZ, no backups — intentionally minimal). Each app (cms, discovery) gets its own login role and same-named database via the postgresql provider (newServiceDatabase), so services never share DB credentials.
  • ECS Fargate: one cluster (app-cluster), one ALB per service (each gets its own DNS name rather than sharing a load balancer on different ports). deployFargateService(...) is the shared helper building a service's ECR repo, CloudWatch log group, task definition, ECS service, and ALB. taskRole is optional (nil = no AWS identity beyond the shared execution role); extraEnv appends container env vars beyond the DB_* set.
  • S3 + CloudFront: encoded-bucket holds finished transcoded output, served publicly via CloudFront using Origin Access Control (OAC) — the bucket itself blocks all public access; only CloudFront's OAC principal can read it.
  • S3 raw uploads: raw-uploads-bucket is a separate, private bucket for pre-transcode uploads — deliberately kept apart from encoded-bucket so raw source video is never reachable through the public CDN. CORS is scoped to PUT only, from the cms ALB's own origin (browser uploads directly via presigned URL).
  • IAM roles — four distinct roles/users, each scoped narrowly, don't conflate them:
    • ecs-task-execution-role — shared by both services' ECS agent (image pull, log write, Secrets Manager read for DB password). Not usable by application code inside the container.
    • cms-task-role — the cms container's own AWS identity: S3 ListBucket/PutObject/GetObject on raw-uploads-bucket only, mediaconvert:CreateJob, and iam:PassRole scoped to the MediaConvert service role (iam:PassedToService condition). discovery has no task role — it doesn't touch S3 or MediaConvert.
    • mediaconvert-service-role — trusted by mediaconvert.amazonaws.com, not by ECS; the role MediaConvert itself assumes (passed as CreateJobInput.Role) to read raw-uploads-bucket and write encoded-bucket. Distinct from cms-task-role by design: one is "cms calling AWS", the other is "AWS calling AWS on cms's behalf".
    • gitea-ci-user — an IAM user (static access keys, not OIDC — the Gitea Actions runner doesn't support instance-profile auth) scoped to just ECR push (cms/discovery repos) and ecs:UpdateService/ecs:DescribeServices on the two ECS services. Never broaden this to ecr:*/ecs:*.

CI/CD (.gitea/workflows/)

  • cms-deploy.yml: push to main touching cms/**. Builds/pushes the Docker image to ECR using gitea-ci-user, installs the AWS CLI (not preinstalled on the runner image — via AWS's official install script, not a third-party action), then aws ecs update-service --force-new-deployment. Cluster/service ARNs come from pulumi stack output and are set as repo variables (not secrets — ARNs aren't sensitive).
  • infrastructure-deploy.yml: push to main touching infrastructure/**. Runs pulumi up using a separate, broader AWS credential (PULUMI_AWS_ACCESS_KEY_ID/SECRET) than gitea-ci-user, since provisioning IAM/RDS/ECS/CloudFront needs wider permissions than pushing images and forcing deployments. Guarded with a concurrency group since Pulumi state isn't safe to update concurrently.
  • The runner's ubuntu-latest label maps to a docker image configured on the runner host (outside this repo) — currently minimal, lacking the AWS CLI, hence the manual install step in cms-deploy.yml.

Known gaps

  • cms/internal/services/mediaconvert.go never calls DescribeEndpoints and configures no custom MediaConvert endpoint — relies on the SDK's default regional endpoint.
  • discovery has infra provisioned (ECR repo, ECS service, ALB, Postgres DB/role) but no application code.
  • No automated tests exist for cms, discovery, or infrastructure.