Version Naming | 0.7.901 (Checkpoint) #171
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json | |
| # GitHub Actions Workflow: Deploy TomoriBot to AWS ECS | |
| # Note: Secret validation warnings are expected - secrets are configured in GitHub repo settings | |
| name: Deploy TomoriBot | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: # Manual trigger - allows running on any branch | |
| inputs: | |
| environment: | |
| description: "Deployment environment" | |
| required: false | |
| type: choice | |
| options: | |
| - test | |
| - production | |
| default: test | |
| create_db_snapshot: | |
| description: "Create a pre-deploy RDS snapshot" | |
| required: false | |
| type: boolean | |
| default: false | |
| # OIDC permissions for AWS authentication | |
| permissions: | |
| id-token: write | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| # SECRETS VALIDATION | |
| validate-secrets: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Validate deployment infrastructure secrets | |
| run: | | |
| echo "==========================================" | |
| echo "Validating GitHub Secrets (Infrastructure Only)" | |
| echo "==========================================" | |
| echo "" | |
| echo "NOTE: This workflow ONLY validates deployment/infrastructure secrets." | |
| echo "Application secrets (DISCORD_TOKEN, POSTGRES_PASSWORD, etc.) are fetched" | |
| echo "by the bot from AWS Secrets Manager at runtime via secretsManager.ts." | |
| echo "" | |
| missing_secrets=() | |
| # Infrastructure secrets required for deployment | |
| echo "Checking infrastructure secrets..." | |
| if [ -z "${{ secrets.AWS_ROLE_ARN }}" ]; then missing_secrets+=("AWS_ROLE_ARN"); fi | |
| if [ -z "${{ secrets.AWS_REGION }}" ]; then missing_secrets+=("AWS_REGION"); fi | |
| if [ -z "${{ secrets.ECR_REPOSITORY_NAME }}" ]; then missing_secrets+=("ECR_REPOSITORY_NAME"); fi | |
| if [ -z "${{ secrets.RDS_MASTER_PASSWORD }}" ]; then missing_secrets+=("RDS_MASTER_PASSWORD"); fi | |
| if [ -z "${{ secrets.PAT_TOKEN }}" ]; then missing_secrets+=("PAT_TOKEN"); fi | |
| # Optional secrets | |
| optional_secrets=() | |
| if [ -z "${{ secrets.DISCORD_WEBHOOK_URL }}" ]; then optional_secrets+=("DISCORD_WEBHOOK_URL"); fi | |
| if [ ${#missing_secrets[@]} -gt 0 ]; then | |
| echo "" | |
| echo "Missing required infrastructure secrets: ${missing_secrets[*]}" | |
| echo "" | |
| echo "These secrets are needed by GitHub Actions to deploy to AWS." | |
| echo "Add them in: GitHub Repository → Settings → Secrets and variables → Actions" | |
| exit 1 | |
| fi | |
| if [ ${#optional_secrets[@]} -gt 0 ]; then | |
| echo " Optional secrets not configured: ${optional_secrets[*]}" | |
| fi | |
| echo "" | |
| echo "All required infrastructure secrets are configured!" | |
| echo "" | |
| echo "================================================" | |
| echo "Infrastructure Secrets: VALIDATED" | |
| echo "Application Secrets: Fetched from AWS at runtime" | |
| echo "================================================" | |
| # PARALLEL TESTING PHASE | |
| test-lint: | |
| runs-on: ubuntu-latest | |
| needs: validate-secrets | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v1 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: | | |
| echo "Installing dependencies..." | |
| bun install | |
| echo "Dependencies installed!" | |
| - name: Run linting and type checks | |
| run: | | |
| echo "Running linting and TypeScript checks..." | |
| bun run lint | |
| bun run check | |
| echo "All linting and type checks passed!" | |
| test-locales: | |
| runs-on: ubuntu-latest | |
| needs: validate-secrets | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v1 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Check localization keys | |
| run: | | |
| echo "Checking localization keys..." | |
| output=$(bun run check-locales 2>&1) | |
| exitCode=$? | |
| # Always show the output | |
| echo "$output" | |
| # Check if there are missing keys (critical errors) | |
| if echo "$output" | grep -q "MISSING LOCALIZATION KEYS"; then | |
| echo "Critical error: Missing localization keys found!" | |
| exit 1 | |
| elif [ $exitCode -eq 0 ]; then | |
| echo "All localization checks passed!" | |
| else | |
| echo "Localization warnings found, but no critical missing keys. Continuing deployment..." | |
| fi | |
| # BUILD DOCKER IMAGE | |
| build-image: | |
| runs-on: ubuntu-latest | |
| needs: [test-lint, test-locales] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Pre-download Python packages with platform-specific wheels | |
| run: | | |
| echo "Downloading Python packages (musllinux wheels for Alpine 3.20 Python 3.12)..." | |
| # Create directory for pip packages | |
| mkdir -p docker-pip-cache | |
| # Download platform-specific wheels for Alpine Linux (musllinux) | |
| # Target Python 3.12 (used by Alpine 3.20) with musllinux_1_2 compatibility | |
| pip3 download \ | |
| --dest docker-pip-cache \ | |
| --platform musllinux_1_2_x86_64 \ | |
| --platform musllinux_1_1_x86_64 \ | |
| --python-version 3.12 \ | |
| --implementation cp \ | |
| --abi cp312 \ | |
| --only-binary :all: \ | |
| mcp-server-fetch==2025.4.7 | |
| echo "Packages downloaded to docker-pip-cache/" | |
| ls -lah docker-pip-cache/ | |
| - name: Build Docker image | |
| run: | | |
| echo "Building TomoriBot Docker image..." | |
| docker buildx build \ | |
| --platform linux/amd64 \ | |
| --tag tomoribot:latest \ | |
| --tag tomoribot:${{ github.sha }} \ | |
| --load \ | |
| . | |
| - name: Save Docker image | |
| run: | | |
| echo "Saving Docker image for security scanning..." | |
| docker save tomoribot:latest -o tomoribot-image.tar | |
| - name: Upload Docker image artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: docker-image | |
| path: tomoribot-image.tar | |
| retention-days: 1 | |
| # PHASE 4: SECURITY SCANNING | |
| security-sast: | |
| runs-on: ubuntu-latest | |
| needs: build-image | |
| outputs: | |
| failed: ${{ steps.check-result.outputs.failed }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Run Semgrep SAST scan | |
| uses: returntocorp/semgrep-action@v1 | |
| with: | |
| config: >- | |
| p/security-audit | |
| p/typescript | |
| p/docker | |
| continue-on-error: true | |
| id: semgrep | |
| - name: Check Semgrep results | |
| id: check-result | |
| run: | | |
| if [ "${{ steps.semgrep.outcome }}" == "failure" ]; then | |
| echo "Semgrep found potential security issues. Review the output above." | |
| echo "failed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "SAST scan passed" | |
| echo "failed=false" >> $GITHUB_OUTPUT | |
| fi | |
| security-dependencies: | |
| runs-on: ubuntu-latest | |
| needs: build-image | |
| outputs: | |
| failed: ${{ steps.check-result.outputs.failed }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Bun | |
| uses: oven-sh/setup-bun@v1 | |
| with: | |
| bun-version: latest | |
| - name: Install dependencies | |
| run: bun install | |
| - name: Run Bun audit | |
| run: | | |
| echo "Running dependency vulnerability scan..." | |
| # Run bun audit and capture output | |
| if ! bun audit --audit-level=high > audit-output.txt 2>&1; then | |
| echo "High or critical vulnerabilities found in dependencies:" | |
| cat audit-output.txt | |
| exit 1 | |
| else | |
| echo "No high or critical vulnerabilities found in dependencies" | |
| cat audit-output.txt | |
| fi | |
| continue-on-error: true | |
| id: bun-audit | |
| - name: Check audit results | |
| id: check-result | |
| run: | | |
| if [ "${{ steps.bun-audit.outcome }}" == "failure" ]; then | |
| echo "failed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "failed=false" >> $GITHUB_OUTPUT | |
| fi | |
| security-secrets: | |
| runs-on: ubuntu-latest | |
| needs: build-image | |
| outputs: | |
| failed: ${{ steps.check-result.outputs.failed }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Fetch full history for TruffleHog | |
| - name: Run TruffleHog secret scan | |
| uses: trufflesecurity/trufflehog@main | |
| with: | |
| path: ./ | |
| base: ${{ github.event.before }} | |
| head: ${{ github.event.after }} | |
| extra_args: --only-verified | |
| continue-on-error: true | |
| id: trufflehog | |
| - name: Check TruffleHog results | |
| id: check-result | |
| run: | | |
| if [ "${{ steps.trufflehog.outcome }}" == "failure" ]; then | |
| echo "TruffleHog found exposed secrets in git history!" | |
| echo "failed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "Secret scan passed" | |
| echo "failed=false" >> $GITHUB_OUTPUT | |
| fi | |
| security-container: | |
| runs-on: ubuntu-latest | |
| needs: build-image | |
| outputs: | |
| failed: ${{ steps.check-result.outputs.failed }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Download Docker image artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: docker-image | |
| - name: Load Docker image | |
| run: docker load -i tomoribot-image.tar | |
| - name: Run Trivy container scan | |
| uses: aquasecurity/trivy-action@master | |
| with: | |
| image-ref: tomoribot:latest | |
| format: table | |
| exit-code: 1 | |
| severity: CRITICAL,HIGH | |
| ignore-unfixed: true | |
| trivyignores: .trivyignore | |
| continue-on-error: true | |
| id: trivy | |
| - name: Check Trivy results | |
| id: check-result | |
| run: | | |
| if [ "${{ steps.trivy.outcome }}" == "failure" ]; then | |
| echo "Trivy found critical or high vulnerabilities in container image!" | |
| echo "failed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "Container scan passed" | |
| echo "failed=false" >> $GITHUB_OUTPUT | |
| fi | |
| # SECURITY GATE: Block deployment if critical/high vulnerabilities found | |
| security-gate: | |
| runs-on: ubuntu-latest | |
| needs: | |
| [ | |
| security-sast, | |
| security-dependencies, | |
| security-secrets, | |
| security-container, | |
| ] | |
| steps: | |
| - name: Check security scan results | |
| run: | | |
| echo "==========================================" | |
| echo "Security Gate - Evaluating Scan Results" | |
| echo "==========================================" | |
| echo "" | |
| failed=false | |
| # Check each security scan output | |
| echo "SAST Scan (Semgrep): ${{ needs.security-sast.outputs.failed }}" | |
| if [ "${{ needs.security-sast.outputs.failed }}" == "true" ]; then | |
| echo "SAST scan FAILED - Semgrep found security issues" | |
| failed=true | |
| else | |
| echo "SAST scan passed" | |
| fi | |
| echo "" | |
| echo "Dependency Scan (Bun Audit): ${{ needs.security-dependencies.outputs.failed }}" | |
| if [ "${{ needs.security-dependencies.outputs.failed }}" == "true" ]; then | |
| echo "Dependency scan FAILED - High/Critical CVEs found" | |
| failed=true | |
| else | |
| echo "Dependency scan passed" | |
| fi | |
| echo "" | |
| echo "Secret Scan (TruffleHog): ${{ needs.security-secrets.outputs.failed }}" | |
| if [ "${{ needs.security-secrets.outputs.failed }}" == "true" ]; then | |
| echo "Secret scan FAILED - Exposed secrets found in git history" | |
| failed=true | |
| else | |
| echo "Secret scan passed" | |
| fi | |
| echo "" | |
| echo "Container Scan (Trivy): ${{ needs.security-container.outputs.failed }}" | |
| if [ "${{ needs.security-container.outputs.failed }}" == "true" ]; then | |
| echo "Container scan FAILED - Critical/High vulnerabilities in image" | |
| failed=true | |
| else | |
| echo "Container scan passed" | |
| fi | |
| echo "" | |
| echo "==========================================" | |
| if [ "$failed" == "true" ]; then | |
| echo "DEPLOYMENT BLOCKED" | |
| echo "==========================================" | |
| echo "" | |
| echo "Security scans found critical or high severity issues." | |
| echo "Please review the scan outputs above and fix the issues before deploying." | |
| echo "" | |
| exit 1 | |
| else | |
| echo "ALL SECURITY SCANS PASSED" | |
| echo "==========================================" | |
| echo "" | |
| echo "Proceeding with deployment to AWS..." | |
| fi | |
| # PHASE 5: AWS DEPLOYMENT | |
| push-to-ecr: | |
| runs-on: ubuntu-latest | |
| needs: security-gate | |
| outputs: | |
| image_uri: ${{ steps.ecr_uri.outputs.uri }} | |
| deploy: ${{ steps.ecr_uri.outputs.deploy }} | |
| version: ${{ steps.package_version.outputs.version }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Download Docker image artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: docker-image | |
| - name: Load Docker image | |
| run: docker load -i tomoribot-image.tar | |
| - name: Configure AWS Credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: ${{ secrets.AWS_ROLE_ARN }} | |
| aws-region: ${{ secrets.AWS_REGION }} | |
| - name: Login to Amazon ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@v2 | |
| - name: Get version from package.json | |
| id: package_version | |
| run: | | |
| VERSION=$(jq -r '.version' package.json) | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| echo "Package version: v$VERSION" | |
| - name: Check if version exists in ECR | |
| id: version_check | |
| env: | |
| ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} | |
| ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY_NAME }} | |
| VERSION: ${{ steps.package_version.outputs.version }} | |
| run: | | |
| # Check if version tag exists in ECR | |
| if aws ecr describe-images \ | |
| --registry-id ${ECR_REGISTRY%%.*} \ | |
| --repository-name $ECR_REPOSITORY \ | |
| --image-ids imageTag=v$VERSION \ | |
| >/dev/null 2>&1; then | |
| echo "version_exists=true" >> $GITHUB_OUTPUT | |
| echo " Version v$VERSION already exists in ECR - skipping deployment" | |
| else | |
| echo "version_exists=false" >> $GITHUB_OUTPUT | |
| echo "Version v$VERSION is new - will deploy" | |
| fi | |
| - name: Tag and push image to ECR | |
| id: ecr_uri | |
| env: | |
| ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} | |
| ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY_NAME }} | |
| IMAGE_TAG: ${{ github.sha }} | |
| VERSION: ${{ steps.package_version.outputs.version }} | |
| VERSION_EXISTS: ${{ steps.version_check.outputs.version_exists }} | |
| run: | | |
| echo "Tagging image with commit hash..." | |
| docker tag tomoribot:latest $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG | |
| docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG | |
| echo "Pushed commit hash tag: $IMAGE_TAG" | |
| # Always update 'latest' tag for debugging | |
| docker tag tomoribot:latest $ECR_REGISTRY/$ECR_REPOSITORY:latest | |
| docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest | |
| echo "Updated 'latest' tag" | |
| # Tag with semantic version if new | |
| if [ "$VERSION_EXISTS" = "false" ]; then | |
| echo "Tagging image with semantic version..." | |
| docker tag tomoribot:latest $ECR_REGISTRY/$ECR_REPOSITORY:v$VERSION | |
| docker push $ECR_REGISTRY/$ECR_REPOSITORY:v$VERSION | |
| echo "Pushed version tag: v$VERSION" | |
| # Use version tag for deployment | |
| echo "uri=$ECR_REGISTRY/$ECR_REPOSITORY:v$VERSION" >> $GITHUB_OUTPUT | |
| echo "deploy=true" >> $GITHUB_OUTPUT | |
| else | |
| echo " Skipping version tag (already exists)" | |
| echo "deploy=false" >> $GITHUB_OUTPUT | |
| # Output commit hash as fallback (won't be used for deployment) | |
| echo "uri=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Deployment summary | |
| env: | |
| VERSION: ${{ steps.package_version.outputs.version }} | |
| DEPLOY: ${{ steps.ecr_uri.outputs.deploy }} | |
| run: | | |
| echo "==================================" | |
| echo "Version: v$VERSION" | |
| echo "Deploy: $DEPLOY" | |
| if [ "$DEPLOY" = "true" ]; then | |
| echo "Deployment will proceed with version v$VERSION" | |
| else | |
| echo "Deployment skipped - version v$VERSION already exists" | |
| echo "To deploy, increment version in package.json" | |
| fi | |
| echo "==================================" | |
| deploy-with-terraform: | |
| runs-on: ubuntu-latest | |
| needs: push-to-ecr | |
| # Only deploy if version is new (not already in ECR) | |
| if: needs['push-to-ecr'].outputs.deploy == 'true' | |
| env: | |
| TF_VAR_container_image: ${{ needs['push-to-ecr'].outputs.image_uri }} | |
| TF_VAR_rds_master_password: ${{ secrets.RDS_MASTER_PASSWORD }} | |
| TF_IN_AUTOMATION: "1" | |
| TF_INPUT: "0" | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Configure AWS Credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: ${{ secrets.AWS_ROLE_ARN }} | |
| aws-region: ${{ secrets.AWS_REGION }} | |
| - name: Resolve image URI | |
| env: | |
| AWS_REGION: ${{ secrets.AWS_REGION }} | |
| ECR_REPOSITORY_NAME: ${{ secrets.ECR_REPOSITORY_NAME }} | |
| run: | | |
| if [ -z "${TF_VAR_container_image}" ]; then | |
| ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) | |
| IMAGE_URI="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY_NAME}:${GITHUB_SHA}" | |
| echo "TF_VAR_container_image=$IMAGE_URI" >> $GITHUB_ENV | |
| TF_VAR_container_image="$IMAGE_URI" | |
| echo "Resolved image URI from AWS account: ${TF_VAR_container_image}" | |
| else | |
| echo "Using image URI from push-to-ecr output: ${TF_VAR_container_image}" | |
| fi | |
| - name: Verify image exists in ECR | |
| env: | |
| ECR_REPOSITORY_NAME: ${{ secrets.ECR_REPOSITORY_NAME }} | |
| run: | | |
| aws ecr describe-images \ | |
| --repository-name "$ECR_REPOSITORY_NAME" \ | |
| --image-ids imageTag="$GITHUB_SHA" \ | |
| --query 'imageDetails[0].imageTags' \ | |
| --output text >/dev/null | |
| - name: Validate image URI | |
| run: | | |
| if [ -z "${TF_VAR_container_image}" ]; then | |
| echo "TF_VAR_container_image is empty. Re-run from push-to-ecr or full workflow." | |
| exit 1 | |
| fi | |
| echo "Deploying image: ${TF_VAR_container_image}" | |
| - name: Validate Secrets Manager database target | |
| env: | |
| AWS_REGION: ${{ secrets.AWS_REGION }} | |
| run: | | |
| secret_json=$(aws secretsmanager get-secret-value \ | |
| --secret-id tomoribot/production \ | |
| --query SecretString \ | |
| --output text) | |
| secret_host=$(echo "$secret_json" | jq -r '.POSTGRES_HOST // empty') | |
| secret_db=$(echo "$secret_json" | jq -r '.POSTGRES_DB // empty') | |
| if [ -z "$secret_host" ] || [ -z "$secret_db" ]; then | |
| echo "Secrets Manager is missing POSTGRES_HOST or POSTGRES_DB. Aborting deploy." | |
| exit 1 | |
| fi | |
| if [ "$secret_host" != "tomoribot-db.cszuse2muutq.us-east-1.rds.amazonaws.com" ]; then | |
| echo "POSTGRES_HOST does not match the expected RDS endpoint. Aborting deploy." | |
| exit 1 | |
| fi | |
| if [ "$secret_db" != "tomoribot" ]; then | |
| echo "POSTGRES_DB does not match expected value. Aborting deploy." | |
| exit 1 | |
| fi | |
| echo "Secrets Manager database target validated." | |
| - name: Setup Terraform | |
| uses: hashicorp/setup-terraform@v3 | |
| - name: Optional RDS snapshot (manual or (Checkpoint) commit) | |
| if: | | |
| (github.event_name == 'workflow_dispatch' && inputs.create_db_snapshot == true) || | |
| (github.event_name == 'push' && contains(github.event.head_commit.message, '(Checkpoint)')) | |
| run: | | |
| SNAPSHOT_ID="tomoribot-db-pre-deploy-${{ github.run_id }}-${{ github.run_attempt }}" | |
| aws rds create-db-snapshot \ | |
| --db-instance-identifier tomoribot-db \ | |
| --db-snapshot-identifier "$SNAPSHOT_ID" | |
| - name: Terraform init | |
| working-directory: terraform | |
| run: terraform init | |
| - name: Terraform plan | |
| working-directory: terraform | |
| run: terraform plan -input=false -var-file=terraform.ci.tfvars -out=tfplan | |
| - name: Guard against RDS destroy/replace | |
| working-directory: terraform | |
| run: | | |
| echo "Checking plan for destructive RDS changes..." | |
| if terraform show -json tfplan | jq -e '.resource_changes[] | |
| | select(.type=="aws_db_instance") | |
| | select(.change.actions | index("delete"))' >/dev/null; then | |
| echo "RDS instance would be destroyed or replaced. Aborting deploy." | |
| exit 1 | |
| fi | |
| - name: Terraform apply | |
| working-directory: terraform | |
| run: terraform apply -input=false -auto-approve tfplan | |
| verify-ecs-deployment: | |
| runs-on: ubuntu-latest | |
| needs: deploy-with-terraform | |
| outputs: | |
| deployment-status: ${{ steps.verify.outputs.status }} | |
| commit-hash: ${{ steps.commit-info.outputs.hash }} | |
| commit-message: ${{ steps.commit-info.outputs.message }} | |
| env: | |
| ECS_CLUSTER: tomoribot-cluster | |
| ECS_SERVICE: tomoribot-service | |
| AWS_REGION: ${{ secrets.AWS_REGION }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Get commit information | |
| id: commit-info | |
| run: | | |
| hash=$(git rev-parse --short HEAD) | |
| message=$(git log -1 --pretty=format:"%s") | |
| echo "hash=$hash" >> $GITHUB_OUTPUT | |
| echo "message=$message" >> $GITHUB_OUTPUT | |
| - name: Configure AWS Credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: ${{ secrets.AWS_ROLE_ARN }} | |
| aws-region: ${{ secrets.AWS_REGION }} | |
| - name: Wait for ECS service stability | |
| id: verify | |
| run: | | |
| echo "Waiting for ECS service to stabilize..." | |
| # Wait for service to reach steady state (max 10 minutes) | |
| if aws ecs wait services-stable \ | |
| --cluster $ECS_CLUSTER \ | |
| --services $ECS_SERVICE; then | |
| echo "ECS service is stable!" | |
| echo "status=success" >> $GITHUB_OUTPUT | |
| else | |
| echo "ECS service failed to stabilize!" | |
| echo "status=failed" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| - name: Check ECS task health | |
| if: always() | |
| run: | | |
| echo "Checking ECS task health..." | |
| # Get running tasks | |
| TASKS=$(aws ecs list-tasks \ | |
| --cluster $ECS_CLUSTER \ | |
| --service-name $ECS_SERVICE \ | |
| --desired-status RUNNING \ | |
| --query 'taskArns' \ | |
| --output text) | |
| if [ -z "$TASKS" ]; then | |
| echo "No running tasks found!" | |
| exit 1 | |
| fi | |
| # Describe tasks | |
| aws ecs describe-tasks \ | |
| --cluster $ECS_CLUSTER \ | |
| --tasks $TASKS \ | |
| --query 'tasks[*].[taskArn, lastStatus, healthStatus, containers[0].healthStatus]' \ | |
| --output table | |
| echo "Task health check complete!" | |
| - name: Check recent CloudWatch logs | |
| if: always() | |
| run: | | |
| echo "Fetching recent CloudWatch logs..." | |
| # Get logs from the last 5 minutes | |
| START_TIME=$(($(date +%s) - 300))000 | |
| aws logs tail /ecs/tomoribot \ | |
| --since ${START_TIME} \ | |
| --format short \ | |
| --follow=false \ | |
| || echo "No recent logs found (this is normal for new deployments)" | |
| # PHASE 6: GITHUB RELEASES (Source Code Only) | |
| # Creates GitHub releases with source code and release notes when .github/release/vX.X.X/ folder exists | |
| # Docker images are NOT included - users should clone and use docker-compose.yaml for self-hosting | |
| # Future: May include installer packages (.exe, .app) for easier end-user setup | |
| check-release-assets: | |
| runs-on: ubuntu-latest | |
| needs: verify-ecs-deployment | |
| if: needs.verify-ecs-deployment.outputs.deployment-status == 'success' | |
| outputs: | |
| has-assets: ${{ steps.check.outputs.has-assets }} | |
| version: ${{ steps.version.outputs.version }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Get version from package.json | |
| id: version | |
| run: | | |
| version=$(jq -r '.version' package.json) | |
| echo "version=$version" >> $GITHUB_OUTPUT | |
| - name: Check if version-specific release folder exists | |
| id: check | |
| env: | |
| VERSION: ${{ steps.version.outputs.version }} | |
| run: | | |
| versionFolder=".github/release/v$VERSION" | |
| if [ -d "$versionFolder" ]; then | |
| echo "Version-specific release folder found: $versionFolder" | |
| echo "has-assets=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "No version-specific release folder found: $versionFolder" | |
| echo "This appears to be a minor update that doesn't require a release announcement" | |
| echo "has-assets=false" >> $GITHUB_OUTPUT | |
| fi | |
| create-release: | |
| runs-on: ubuntu-latest | |
| needs: [verify-ecs-deployment, check-release-assets] | |
| if: needs.verify-ecs-deployment.outputs.deployment-status == 'success' && needs.check-release-assets.outputs.has-assets == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Check if GitHub release already exists | |
| id: release-check | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT_TOKEN }} | |
| VERSION: ${{ needs.check-release-assets.outputs.version }} | |
| run: | | |
| if gh release view "v$VERSION" >/dev/null 2>&1; then | |
| echo "Release v$VERSION already exists. Skipping release creation." | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "Release v$VERSION does not exist. Proceeding." | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Prepare release notes and Discord notification | |
| id: release-notes | |
| if: steps.release-check.outputs.exists != 'true' | |
| env: | |
| VERSION: ${{ needs.check-release-assets.outputs.version }} | |
| COMMIT_HASH: ${{ needs.verify-ecs-deployment.outputs.commit-hash }} | |
| run: | | |
| versionFolder=".github/release/v$VERSION" | |
| # Find first .md file in version folder | |
| releaseNotesPath=$(find "$versionFolder" -maxdepth 1 -name "*.md" ! -name "README.md" | head -n 1) | |
| if [ -z "$releaseNotesPath" ]; then | |
| echo "No .md files found in $versionFolder" | |
| exit 1 | |
| fi | |
| echo "Using release notes: $releaseNotesPath" | |
| # Find first image file in version folder | |
| imageName=$(find "$versionFolder" -maxdepth 1 -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.gif" -o -name "*.webp" \) | head -n 1 | xargs basename) | |
| if [ -z "$imageName" ]; then | |
| echo "No image file found, Discord notification will be text-only" | |
| imageName="" | |
| else | |
| echo "Image found: $imageName" | |
| fi | |
| # Read and process release notes | |
| releaseNotes=$(cat "$releaseNotesPath") | |
| releaseNotes="${releaseNotes//\{VERSION\}/$VERSION}" | |
| releaseNotes="${releaseNotes//\{TIMESTAMP\}/$(date -u +"%Y-%m-%d %H:%M:%S UTC")}" | |
| releaseNotes="${releaseNotes//\{COMMIT_HASH\}/$COMMIT_HASH}" | |
| releaseNotes="${releaseNotes//\{REPO_OWNER\}/${{ github.repository_owner }}}" | |
| releaseNotes="${releaseNotes//\{REPO_NAME\}/${{ github.event.repository.name }}}" | |
| # Save processed release notes | |
| echo "$releaseNotes" > processed-release-notes.md | |
| echo "image-name=$imageName" >> $GITHUB_OUTPUT | |
| - name: Create GitHub Release (Source Code Only) | |
| if: steps.release-check.outputs.exists != 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT_TOKEN }} | |
| VERSION: ${{ needs.check-release-assets.outputs.version }} | |
| run: | | |
| echo "Creating GitHub Release v$VERSION (source code only)..." | |
| gh release create "v$VERSION" \ | |
| --title "TomoriBot v$VERSION" \ | |
| --notes-file "processed-release-notes.md" \ | |
| --latest | |
| echo "GitHub release created!" | |
| echo "" | |
| echo "Note: Users can download source code and use 'docker-compose up' for self-hosting." | |
| echo "Future releases may include installer packages (.exe, .app, etc.) for easier setup." | |
| - name: Send Discord notification | |
| if: steps.release-check.outputs.exists != 'true' | |
| env: | |
| STATUS: ${{ needs.verify-ecs-deployment.outputs.deployment-status }} | |
| VERSION: ${{ needs.check-release-assets.outputs.version }} | |
| IMAGE_NAME: ${{ steps.release-notes.outputs.image-name }} | |
| run: | | |
| if [ "$STATUS" == "success" ]; then | |
| echo "Preparing success notification for Discord..." | |
| status="SUCCESS" | |
| color="3066993" # Green | |
| else | |
| echo "Preparing failure notification for Discord..." | |
| status="FAILED" | |
| color="15158332" # Red | |
| fi | |
| # Read and process release notes for Discord | |
| releaseNotes=$(cat processed-release-notes.md) | |
| # Strip markdown image syntax | |
| releaseNotes=$(echo "$releaseNotes" | sed 's/!\[.*\]([^)]*)//g') | |
| # Clean up extra whitespace and truncate for Discord limits | |
| releaseNotes=$(echo "$releaseNotes" | sed '/^$/d') | |
| if [ ${#releaseNotes} -gt 4090 ]; then | |
| releaseNotes="${releaseNotes:0:4090}..." | |
| fi | |
| # Construct image URL | |
| imageUrl="" | |
| if [ -n "$IMAGE_NAME" ]; then | |
| imageUrl="https://github.com/${{ github.repository }}/raw/main/.github/release/v$VERSION/$IMAGE_NAME" | |
| echo "Image URL: $imageUrl" | |
| else | |
| echo "Text-only Discord notification (no image found)" | |
| fi | |
| # Construct Discord webhook payload | |
| if [ -n "$imageUrl" ]; then | |
| payload=$(jq -n \ | |
| --arg title "TomoriBot v$VERSION Released!" \ | |
| --arg desc "$releaseNotes" \ | |
| --arg color "$color" \ | |
| --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")" \ | |
| --arg imageUrl "$imageUrl" \ | |
| '{embeds: [{title: $title, description: $desc, color: ($color | tonumber), timestamp: $timestamp, image: {url: $imageUrl}}]}') | |
| else | |
| payload=$(jq -n \ | |
| --arg title "TomoriBot v$VERSION Released!" \ | |
| --arg desc "$releaseNotes" \ | |
| --arg color "$color" \ | |
| --arg timestamp "$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")" \ | |
| '{embeds: [{title: $title, description: $desc, color: ($color | tonumber), timestamp: $timestamp}]}') | |
| fi | |
| # Send to Discord | |
| discordWebhookUrl="${{ secrets.DISCORD_WEBHOOK_URL }}" | |
| if [ -n "$discordWebhookUrl" ]; then | |
| if curl -X POST -H "Content-Type: application/json" -d "$payload" "$discordWebhookUrl"; then | |
| echo "Discord notification sent successfully!" | |
| else | |
| echo "Failed to send Discord notification" | |
| fi | |
| else | |
| echo "DISCORD_WEBHOOK_URL secret not configured - notification skipped" | |
| echo "Discord payload would be:" | |
| echo "$payload" | |
| fi |