From a956228fd36e48f18bd9fb7009ce002856e4d2e1 Mon Sep 17 00:00:00 2001 From: Yilin Jing Date: Fri, 27 Feb 2026 13:06:55 +0800 Subject: [PATCH 1/5] feat(ci): add Dockerfile.ci for CI pre-built binary deploy --- backend/Dockerfile.ci | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 backend/Dockerfile.ci diff --git a/backend/Dockerfile.ci b/backend/Dockerfile.ci new file mode 100644 index 0000000..1add1a5 --- /dev/null +++ b/backend/Dockerfile.ci @@ -0,0 +1,8 @@ +FROM alpine:3.20 +RUN apk --no-cache add ca-certificates && \ + adduser -D -u 1001 appuser +WORKDIR /app +COPY api . +USER appuser +EXPOSE 8080 +CMD ["./api"] From 6ee54ae1cae83f01cbb9e13a07246ed8446ba760 Mon Sep 17 00:00:00 2001 From: Yilin Jing Date: Fri, 27 Feb 2026 13:07:40 +0800 Subject: [PATCH 2/5] feat(ci): rewrite deploy workflow with OIDC, dev/prod branching, inline task def --- .github/workflows/deploy-backend.yml | 179 ++++++++++++++++++++++----- 1 file changed, 149 insertions(+), 30 deletions(-) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 0379562..8373911 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -1,70 +1,181 @@ -name: Deploy Backend +name: Deploy Backend to AWS ECS on: push: - branches: [main] + branches: + - main + - develop paths: - "backend/**" - ".github/workflows/deploy-backend.yml" + workflow_dispatch: env: AWS_REGION: us-east-1 - ECR_REPOSITORY: kickwatch-backend - ECS_CLUSTER: kickwatch-cluster - ECS_SERVICE: kickwatch-backend-service - CONTAINER_NAME: kickwatch-backend jobs: - deploy: + build-and-deploy: + name: Build and Deploy runs-on: ubuntu-latest - defaults: - run: - working-directory: backend + permissions: + contents: read + id-token: write + + env: + IS_PROD: ${{ github.ref == 'refs/heads/main' }} + steps: + - name: Set environment variables + run: | + if [ "${{ env.IS_PROD }}" = "true" ]; then + echo "ECR_REPOSITORY=kickwatch-api" >> $GITHUB_ENV + echo "ECS_CLUSTER=kickwatch-cluster" >> $GITHUB_ENV + echo "ECS_SERVICE=kickwatch-api-service" >> $GITHUB_ENV + echo "CONTAINER_NAME=kickwatch-api" >> $GITHUB_ENV + echo "DEPLOY_ENV=production" >> $GITHUB_ENV + echo "SECRET_PREFIX=kickwatch" >> $GITHUB_ENV + echo "LOG_GROUP=/ecs/kickwatch-api" >> $GITHUB_ENV + echo "GIN_MODE=release" >> $GITHUB_ENV + else + echo "ECR_REPOSITORY=kickwatch-api-dev" >> $GITHUB_ENV + echo "ECS_CLUSTER=kickwatch-cluster-dev" >> $GITHUB_ENV + echo "ECS_SERVICE=kickwatch-api-dev-service" >> $GITHUB_ENV + echo "CONTAINER_NAME=kickwatch-api-dev" >> $GITHUB_ENV + echo "DEPLOY_ENV=development" >> $GITHUB_ENV + echo "SECRET_PREFIX=kickwatch-dev" >> $GITHUB_ENV + echo "LOG_GROUP=/ecs/kickwatch-api-dev" >> $GITHUB_ENV + echo "GIN_MODE=debug" >> $GITHUB_ENV + fi + - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.24" + go-version-file: backend/go.mod cache-dependency-path: backend/go.sum - - run: go test ./... + - name: Run tests and vet + working-directory: backend + run: | + go vet ./... & + VET_PID=$! + go test ./... & + TEST_PID=$! + wait $VET_PID || exit 1 + wait $TEST_PID || exit 1 + + - name: Build Go binary + working-directory: backend + run: CGO_ENABLED=0 GOOS=linux go build -o api ./cmd/api - - name: Configure AWS credentials + - name: Configure AWS credentials (OIDC) uses: aws-actions/configure-aws-credentials@v4 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} aws-region: ${{ env.AWS_REGION }} - - name: Login to ECR + - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v2 - - name: Build, tag, push image + - name: Ensure ECR repository exists + run: | + aws ecr describe-repositories --repository-names $ECR_REPOSITORY --region $AWS_REGION 2>/dev/null || \ + aws ecr create-repository --repository-name $ECR_REPOSITORY --region $AWS_REGION \ + --image-scanning-configuration scanOnPush=true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image id: build-image + uses: docker/build-push-action@v6 + with: + context: backend + file: backend/Dockerfile.ci + push: true + tags: | + ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }} + ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: false + + - name: Resolve Secrets Manager ARNs + id: secrets env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ github.sha }} + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} run: | - docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + get_arn() { aws secretsmanager describe-secret --secret-id "$1" --region $AWS_REGION --query "ARN" --output text; } + echo "db_arn=$(get_arn ${SECRET_PREFIX}/database-url)" >> $GITHUB_OUTPUT + echo "apns_key_id_arn=$(get_arn ${SECRET_PREFIX}/apns-key-id)" >> $GITHUB_OUTPUT + echo "apns_team_id_arn=$(get_arn ${SECRET_PREFIX}/apns-team-id)" >> $GITHUB_OUTPUT + echo "apns_bundle_id_arn=$(get_arn ${SECRET_PREFIX}/apns-bundle-id)" >> $GITHUB_OUTPUT + echo "apns_key_arn=$(get_arn ${SECRET_PREFIX}/apns-key)" >> $GITHUB_OUTPUT - - name: Download ECS task definition + - name: Generate ECS task definition + env: + AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }} run: | - aws ecs describe-task-definition \ - --task-definition kickwatch-backend \ - --query taskDefinition \ - > task-definition.json + cat > /tmp/task-definition.json <> $GITHUB_STEP_SUMMARY + echo "- **Environment**: ${{ env.DEPLOY_ENV }}" >> $GITHUB_STEP_SUMMARY + echo "- **Cluster**: ${{ env.ECS_CLUSTER }}" >> $GITHUB_STEP_SUMMARY + echo "- **Service**: ${{ env.ECS_SERVICE }}" >> $GITHUB_STEP_SUMMARY + echo "- **Image**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY From efd37c720332f8bf036d17064f13e43d7c62900a Mon Sep 17 00:00:00 2001 From: Yilin Jing Date: Fri, 27 Feb 2026 13:07:44 +0800 Subject: [PATCH 3/5] feat(ci): update test workflow to use go.mod version file and verify build --- .github/workflows/test-backend.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml index 088abf5..10ad409 100644 --- a/.github/workflows/test-backend.yml +++ b/.github/workflows/test-backend.yml @@ -4,9 +4,11 @@ on: push: paths: - "backend/**" + - ".github/workflows/test-backend.yml" pull_request: paths: - "backend/**" + - ".github/workflows/test-backend.yml" jobs: test: @@ -18,8 +20,8 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: "1.24" - cache-dependency-path: backend/go.sum - - run: go build ./... - - run: go test ./... + go-version-file: go.mod + cache-dependency-path: go.sum - run: go vet ./... + - run: go test ./... + - run: CGO_ENABLED=0 GOOS=linux go build -o /dev/null ./cmd/api From bf707418bcabb77f5cbf750c662b0e82444dab78 Mon Sep 17 00:00:00 2001 From: Yilin Jing Date: Fri, 27 Feb 2026 13:11:23 +0800 Subject: [PATCH 4/5] fix(ci): set AWS_REGION to us-east-2 --- .github/workflows/deploy-backend.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 8373911..3244d5f 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -11,7 +11,7 @@ on: workflow_dispatch: env: - AWS_REGION: us-east-1 + AWS_REGION: us-east-2 jobs: build-and-deploy: From 6914ea0c9c287c8d8a18bb668ad456fe07ddf809 Mon Sep 17 00:00:00 2001 From: Yilin Jing Date: Fri, 27 Feb 2026 13:35:34 +0800 Subject: [PATCH 5/5] feat(ci): inject APNS_KEY from Secrets Manager into ECS task --- .github/workflows/deploy-backend.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-backend.yml b/.github/workflows/deploy-backend.yml index 3244d5f..cc0174e 100644 --- a/.github/workflows/deploy-backend.yml +++ b/.github/workflows/deploy-backend.yml @@ -138,14 +138,14 @@ jobs: { "name": "PORT", "value": "8080" }, { "name": "GIN_MODE", "value": "${{ env.GIN_MODE }}" }, { "name": "APP_ENV", "value": "${{ env.DEPLOY_ENV }}" }, - { "name": "APNS_ENV", "value": "${{ env.IS_PROD == 'true' && 'production' || 'sandbox' }}" }, - { "name": "APNS_KEY_PATH","value": "/secrets/apns.p8" } + { "name": "APNS_ENV", "value": "${{ env.IS_PROD == 'true' && 'production' || 'sandbox' }}" } ], "secrets": [ { "name": "DATABASE_URL", "valueFrom": "${{ steps.secrets.outputs.db_arn }}" }, { "name": "APNS_KEY_ID", "valueFrom": "${{ steps.secrets.outputs.apns_key_id_arn }}" }, { "name": "APNS_TEAM_ID", "valueFrom": "${{ steps.secrets.outputs.apns_team_id_arn }}" }, - { "name": "APNS_BUNDLE_ID", "valueFrom": "${{ steps.secrets.outputs.apns_bundle_id_arn }}" } + { "name": "APNS_BUNDLE_ID", "valueFrom": "${{ steps.secrets.outputs.apns_bundle_id_arn }}" }, + { "name": "APNS_KEY", "valueFrom": "${{ steps.secrets.outputs.apns_key_arn }}" } ], "readonlyRootFilesystem": true, "linuxParameters": { "initProcessEnabled": true },