Skip to content

Fix | Noisy Unsupported Channel Embed #26

Fix | Noisy Unsupported Channel Embed

Fix | Noisy Unsupported Channel Embed #26

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
}