Fix | Noisy Unsupported Channel Embed #26
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
| name: Deploy TomoriBot | |
| on: | |
| push: | |
| branches: [ main ] | |
| workflow_dispatch: # Manual trigger | |
| jobs: | |
| # 🔐 SECRETS VALIDATION | |
| validate-secrets: | |
| runs-on: self-hosted | |
| steps: | |
| - name: Validate required secrets | |
| run: | | |
| echo "Validating required secrets..." | |
| $missingSecrets = @() | |
| # Check required secrets | |
| if ("${{ secrets.DISCORD_TOKEN }}" -eq "") { $missingSecrets += "DISCORD_TOKEN" } | |
| if ("${{ secrets.CRYPTO_SECRET }}" -eq "") { $missingSecrets += "CRYPTO_SECRET" } | |
| if ("${{ secrets.POSTGRES_PASSWORD }}" -eq "") { $missingSecrets += "POSTGRES_PASSWORD" } | |
| if ("${{ secrets.GOOGLE_API_KEY }}" -eq "") { $missingSecrets += "GOOGLE_API_KEY" } | |
| if ("${{ secrets.DEEPL_KEY }}" -eq "") { $missingSecrets += "DEEPL_KEY" } | |
| # Check optional secrets | |
| $optionalSecrets = @() | |
| if ("${{ secrets.DISCORD_WEBHOOK_URL }}" -eq "") { $optionalSecrets += "DISCORD_WEBHOOK_URL" } | |
| if ($missingSecrets.Count -gt 0) { | |
| echo "Missing required secrets: $($missingSecrets -join ', ')" | |
| exit 1 | |
| } | |
| if ($optionalSecrets.Count -gt 0) { | |
| echo "Optional secrets not configured: $($optionalSecrets -join ', ')" | |
| } | |
| echo "All required secrets are configured!" | |
| # 🧪 PARALLEL TESTING PHASE | |
| test-lint: | |
| runs-on: self-hosted | |
| needs: validate-secrets | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - 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: self-hosted | |
| needs: validate-secrets | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Check localization keys | |
| run: | | |
| echo "Checking localization keys..." | |
| $output = bun run check-locales 2>&1 | |
| $exitCode = $LASTEXITCODE | |
| # Always show the output | |
| echo $output | |
| # Check if there are missing keys (critical errors) | |
| if ($output -match "❌ MISSING LOCALIZATION KEYS") { | |
| echo "Critical error: Missing localization keys found!" | |
| exit 1 | |
| } elseif ($exitCode -eq 0) { | |
| echo "All localization checks passed!" | |
| } else { | |
| echo "Localization warnings found, but no critical missing keys. Continuing deployment..." | |
| } | |
| # 🚀 SEQUENTIAL DEPLOYMENT CHAIN | |
| setup-environment: | |
| runs-on: self-hosted | |
| needs: [test-lint, test-locales] | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Create environment file | |
| run: | | |
| echo "Creating environment file from secrets..." | |
| $envContent = @" | |
| DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} | |
| CRYPTO_SECRET=${{ secrets.CRYPTO_SECRET }} | |
| POSTGRES_HOST=postgres | |
| POSTGRES_PORT=5432 | |
| POSTGRES_USER=tomori | |
| POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} | |
| POSTGRES_DB=tomodb | |
| DEFAULT_BOTNAME=Tomori | |
| DEFAULT_BOTNAME_JP=ともり | |
| BASE_TRIGGER_WORDS=tomori,tomo,トモリ,ともり | |
| RUN_ENV=production | |
| GOOGLE_API_KEY=${{ secrets.GOOGLE_API_KEY }} | |
| DEEPL_KEY=${{ secrets.DEEPL_KEY }} | |
| DEFAULT_GEMINI_MODEL=gemini-2.5-flash-preview-05-20 | |
| DEFAULT_GEMINI_SUBAGENT_MODEL=gemini-2.5-flash-preview-05-20 | |
| STREAMING_ENABLED=true | |
| PREFIX== | |
| GENCH_ID=877047848330465294 | |
| DEV_ID=684462114022490125 | |
| TESTSRV_ID=877047847214792705 | |
| TESTCH_ID=1135045786699309056 | |
| TOMORI_ID=841644102059556915 | |
| TOMORI_DMS=1112155828263338024 | |
| STRING_FOR_DEV=development | |
| HAVENSRV_ID=1040174444074782802 | |
| "@ | |
| [System.IO.File]::WriteAllText("$PWD\.env", $envContent, [System.Text.Encoding]::UTF8) | |
| - name: Debug environment file | |
| run: | | |
| echo "Environment file contents:" | |
| Get-Content .env | |
| build-image: | |
| runs-on: self-hosted | |
| needs: setup-environment | |
| steps: | |
| - name: Build new image (without Buildkit) | |
| run: | | |
| echo "Building new TomoriBot image..." | |
| $env:DOCKER_BUILDKIT=0 | |
| docker compose build --no-cache | |
| deploy-containers: | |
| runs-on: self-hosted | |
| needs: build-image | |
| steps: | |
| - name: Stop and remove old containers | |
| run: | | |
| echo "Stopping and cleaning up old TomoriBot resources..." | |
| docker compose down --remove-orphans | |
| continue-on-error: true | |
| - name: Wait for Docker cleanup | |
| run: | | |
| echo "Waiting for containers to fully stop..." | |
| $timeout = 60 | |
| $elapsed = 0 | |
| do { | |
| $containers = docker ps -q --filter "name=tomoribot" | |
| if (-not $containers) { | |
| echo "All TomoriBot containers stopped successfully" | |
| break | |
| } | |
| Start-Sleep 2 | |
| $elapsed += 2 | |
| if ($elapsed -ge $timeout) { | |
| echo "Timeout waiting for containers to stop, proceeding anyway..." | |
| break | |
| } | |
| } while ($true) | |
| - name: Start updated containers | |
| run: | | |
| echo "Starting updated TomoriBot..." | |
| docker compose up -d | |
| verify-deployment: | |
| runs-on: self-hosted | |
| needs: deploy-containers | |
| outputs: | |
| deployment-status: ${{ steps.verify.outputs.status }} | |
| commit-hash: ${{ steps.commit-info.outputs.hash }} | |
| commit-message: ${{ steps.commit-info.outputs.message }} | |
| 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" >> $env:GITHUB_OUTPUT | |
| echo "message=$message" >> $env:GITHUB_OUTPUT | |
| - name: Verify deployment | |
| id: verify | |
| run: | | |
| echo "Verifying TomoriBot is running..." | |
| $timeout = 120 | |
| $elapsed = 0 | |
| $checkInterval = 10 | |
| do { | |
| echo "Checking container status... (${elapsed}s elapsed)" | |
| $containers = docker ps --filter "name=tomoribot" --format "table {{.Names}}\t{{.Status}}" | |
| echo $containers | |
| # Check if both containers are running | |
| $appRunning = docker ps --filter "name=tomoribot-app" --filter "status=running" -q | |
| $dbRunning = docker ps --filter "name=tomoribot-db" --filter "status=running" -q | |
| if ($appRunning -and $dbRunning) { | |
| echo "TomoriBot deployment successful!" | |
| echo "status=success" >> $env:GITHUB_OUTPUT | |
| exit 0 | |
| } | |
| if ($elapsed -ge $timeout) { | |
| echo "Timeout reached. TomoriBot deployment failed!" | |
| echo "Final container status:" | |
| docker ps --filter "name=tomoribot" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | |
| echo "status=failed" >> $env:GITHUB_OUTPUT | |
| exit 1 | |
| } | |
| Start-Sleep $checkInterval | |
| $elapsed += $checkInterval | |
| } while ($true) | |
| check-release-assets: | |
| runs-on: self-hosted | |
| needs: verify-deployment | |
| if: needs.verify-deployment.outputs.deployment-status == 'success' | |
| outputs: | |
| has-assets: ${{ steps.check.outputs.has-assets }} | |
| version: ${{ needs.check-release-assets.outputs.version }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Get version from package.json | |
| id: version | |
| run: | | |
| $version = (Get-Content package.json | ConvertFrom-Json).version | |
| echo "version=$version" >> $env:GITHUB_OUTPUT | |
| - name: Check if version-specific release folder exists | |
| id: check | |
| run: | | |
| $version = "${{ needs.check-release-assets.outputs.version }}" | |
| $versionFolder = ".github/release/v$version" | |
| if (Test-Path $versionFolder) { | |
| echo "Version-specific release folder found: $versionFolder" | |
| echo "has-assets=true" >> $env: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" >> $env:GITHUB_OUTPUT | |
| } | |
| announce-release: | |
| runs-on: self-hosted | |
| needs: [verify-deployment, check-release-assets] | |
| if: needs.verify-deployment.outputs.deployment-status == 'success' && needs.check-release-assets.outputs.has-assets == 'true' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Prepare release notes | |
| id: release-notes | |
| run: | | |
| $version = "${{ needs.check-release-assets.outputs.version }}" | |
| $versionFolder = ".github/release/v$version" | |
| # Function to find first .md file in a folder | |
| function Find-MarkdownFile($folder) { | |
| if (Test-Path $folder) { | |
| $mdFiles = Get-ChildItem -Path $folder -Filter "*.md" | Where-Object { $_.Name -ne "README.md" } | |
| if ($mdFiles) { | |
| return $mdFiles[0].FullName | |
| } | |
| } | |
| return $null | |
| } | |
| # Function to find first image file in a folder | |
| function Find-ImageFile($folder) { | |
| if (Test-Path $folder) { | |
| $imageFiles = Get-ChildItem -Path $folder -Include "*.png","*.jpg","*.jpeg","*.gif","*.webp" -Recurse | |
| if ($imageFiles) { | |
| return $imageFiles[0].Name | |
| } | |
| } | |
| return $null | |
| } | |
| # Use version-specific assets only | |
| $releaseNotesPath = Find-MarkdownFile $versionFolder | |
| if ($releaseNotesPath) { | |
| $assetFolder = $versionFolder | |
| $imageName = Find-ImageFile $versionFolder | |
| echo "Using version-specific assets from $versionFolder" | |
| echo " Release notes: $(Split-Path -Leaf $releaseNotesPath)" | |
| echo " Image: $imageName" | |
| } else { | |
| echo "No .md files found in $versionFolder" | |
| exit 1 | |
| } | |
| if (-not $imageName) { | |
| echo "No image file found, Discord notification will be text-only" | |
| $imageName = "" | |
| } | |
| # Read the release notes template | |
| $releaseNotes = Get-Content $releaseNotesPath -Raw | |
| # Replace all placeholders | |
| $releaseNotes = $releaseNotes.Replace("{VERSION}", $version) | |
| $releaseNotes = $releaseNotes.Replace("{TIMESTAMP}", (Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC")) | |
| $releaseNotes = $releaseNotes.Replace("{COMMIT_HASH}", "${{ needs.verify-deployment.outputs.commit-hash }}") | |
| $releaseNotes = $releaseNotes.Replace("{REPO_OWNER}", "${{ github.repository_owner }}") | |
| $releaseNotes = $releaseNotes.Replace("{REPO_NAME}", "${{ github.event.repository.name }}") | |
| # Save processed release notes and asset info | |
| [System.IO.File]::WriteAllText("$PWD\processed-release-notes.md", $releaseNotes, [System.Text.Encoding]::UTF8) | |
| echo "image-name=$imageName" >> $env:GITHUB_OUTPUT | |
| echo "Release notes processed successfully!" | |
| - name: Export Docker image | |
| run: | | |
| echo "Exporting Docker image as release asset..." | |
| # Get the image name and tag it with version | |
| $imageName = "tomoribot" | |
| $version = "${{ needs.check-release-assets.outputs.version }}" | |
| # Tag the current image with version (Docker Compose creates images as project-service format) | |
| docker tag "tomoribot-tomoribot:latest" "${imageName}:v${version}" | |
| # Export Docker image to tar file | |
| docker save "${imageName}:v${version}" -o "tomoribot-v${version}-docker-image.tar" | |
| # Compress the tar file to reduce size | |
| Compress-Archive -Path "tomoribot-v${version}-docker-image.tar" -DestinationPath "tomoribot-v${version}-docker-image.tar.gz" | |
| # Clean up uncompressed file | |
| Remove-Item "tomoribot-v${version}-docker-image.tar" | |
| echo "Docker image exported and compressed!" | |
| - name: Create GitHub Release | |
| run: | | |
| echo "Creating GitHub Release v${{ needs.check-release-assets.outputs.version }}..." | |
| # Create release with Docker image as asset | |
| gh release create "v${{ needs.check-release-assets.outputs.version }}" ` | |
| --title "TomoriBot v${{ needs.check-release-assets.outputs.version }}" ` | |
| --notes-file "processed-release-notes.md" ` | |
| --latest ` | |
| "tomoribot-v${{ needs.check-release-assets.outputs.version }}-docker-image.tar.gz#Docker Image (Ready to Deploy)" | |
| echo "GitHub release created with Docker image asset!" | |
| env: | |
| GH_TOKEN: ${{ secrets.PAT_TOKEN }} | |
| - name: Send Discord notification | |
| if: always() | |
| run: | | |
| if ("${{ needs.verify-deployment.outputs.deployment-status }}" -eq "success") { | |
| echo "Preparing success notification for Discord..." | |
| $status = "SUCCESS" | |
| $color = "3066993" # Green | |
| } else { | |
| echo "Preparing failure notification for Discord..." | |
| $status = "FAILED" | |
| $color = "15158332" # Red | |
| } | |
| # Read and process release notes for Discord | |
| $releaseNotes = Get-Content processed-release-notes.md -Raw | |
| # Strip markdown image syntax for cleaner Discord text | |
| $releaseNotes = $releaseNotes -replace '!\[.*?\]\([^)]*\)', '' | |
| # Clean up extra whitespace and truncate for Discord limits | |
| $releaseNotes = $releaseNotes -replace '\n\s*\n', "`n`n" # Remove excessive line breaks | |
| if ($releaseNotes.Length -gt 4090) { | |
| $releaseNotes = $releaseNotes.Substring(0, 4090) + "..." | |
| } | |
| # Construct image URL from detected file | |
| $version = "${{ needs.check-release-assets.outputs.version }}" | |
| $imageName = "${{ steps.release-notes.outputs.image-name }}" | |
| $imageUrl = "" | |
| if ($imageName) { | |
| $imageUrl = "https://github.com/${{ github.repository }}/raw/main/.github/release/v$version/$imageName" | |
| echo "Image URL: $imageUrl" | |
| } else { | |
| echo "Text-only Discord notification (no image found)" | |
| } | |
| # Construct Discord webhook payload | |
| $embed = @{ | |
| title = "TomoriBot v$version Released!" | |
| description = $releaseNotes | |
| color = [int]$color | |
| timestamp = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ss.fffZ") | |
| } | |
| # Add image if available | |
| if ($imageUrl) { | |
| $embed.image = @{ url = $imageUrl } | |
| } | |
| $webhookData = @{ | |
| embeds = @($embed) | |
| } | ConvertTo-Json -Depth 10 | |
| # Send to Discord (placeholder - requires DISCORD_WEBHOOK_URL secret) | |
| $discordWebhookUrl = "${{ secrets.DISCORD_WEBHOOK_URL }}" | |
| if ($discordWebhookUrl -ne "") { | |
| try { | |
| Invoke-RestMethod -Uri $discordWebhookUrl -Method Post -Body $webhookData -ContentType "application/json" | |
| echo "Discord notification sent successfully!" | |
| } catch { | |
| echo "Failed to send Discord notification: $($_.Exception.Message)" | |
| } | |
| } else { | |
| echo "DISCORD_WEBHOOK_URL secret not configured - notification skipped" | |
| echo "Discord payload would be:" | |
| echo $webhookData | |
| } |