ci(deploy): auto-deploy oracle-app to KVM2 on merge#26
Conversation
Adds deploy-to-kvm2 job that SSHes into KVM2 root after build-push succeeds, pulls the new ghcr.io image, force-recreates the oracle container, and verifies it's healthy. Eliminates the manual docker compose pull && up -d step (PAI Orch task pai-p3-infra-007). Mirrors the proven Pennyworth Deploy workflow pattern with one retry on failure. Verification uses healthcheck poll (not sleep) and a single coalesced docker inspect. Requires repo secrets KVM2_HOST + KVM2_ROOT_SSH_KEY (both set). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe pull request adds a post-build deployment job to the Docker workflow that connects to KVM2 via SSH, pulls and force-recreates the Changes
Sequence Diagram(s)sequenceDiagram
participant GitHub as GitHub Actions
participant SSH as KVM2 Host<br/>(SSH)
participant Docker as Docker<br/>Engine
participant Container as Oracle<br/>Container
GitHub->>SSH: SSH Connect (secrets)
SSH-->>GitHub: Connected
GitHub->>Docker: Pull latest image & recreate<br/>container (force)
Docker-->>Container: Container running
loop Health Check Polling<br/>(bounded)
Docker->>Container: Check healthcheck status
Container-->>Docker: Health status
Docker-->>GitHub: Status response
alt Healthy
GitHub->>GitHub: Exit loop
end
end
GitHub->>Docker: Inspect container state<br/>& health
Docker-->>GitHub: Container details
alt Running + Healthy
GitHub->>GitHub: Deployment success
else Not healthy or not running
GitHub->>GitHub: Trigger retry job
Note over GitHub: Re-run pull/recreate<br/>& health verification
end
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
.github/workflows/docker.yml (1)
69-99: ⚖️ Poor tradeoffConsider extracting shared deployment logic to reduce duplication.
The retry step duplicates ~20 lines of the primary deployment logic. While acceptable for CI scripts, you could extract this to a script file in the repo (e.g.,
deploy/deploy-oracle.sh) and call it from both steps, improving maintainability.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/docker.yml around lines 69 - 99, The retry step duplicates the entire SSH script block (the commands using docker compose pull, docker compose up -d --force-recreate oracle, the health-check loop that inspects the oracle container and the read into STATUS IMAGE_ID CREATED HEALTH), so extract that script block into a single reusable shell script (for example, deploy-oracle.sh) committed to the repo and replace both SSH-action steps to call that script via the same appleboy/ssh-action invocation; ensure the new script preserves the set -e, the for loop health check and the final status/health logging and exit codes so the behavior stays identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/docker.yml:
- Around line 51-67: The script uses bash-only process substitution in the line
starting with "read STATUS IMAGE_ID CREATED HEALTH < <(docker inspect oracle
--format ...)" which will fail under POSIX shells (e.g., dash); fix it by either
forcing bash for the remote execution (wrap the entire script in bash -c '...')
or replace that read/< <(...) usage with a portable form that captures docker
inspect output into a variable and splits it into STATUS, IMAGE_ID, CREATED,
HEALTH using IFS/positional expansion (and apply the same change for the other
occurrence in the "Retry on failure" step).
---
Nitpick comments:
In @.github/workflows/docker.yml:
- Around line 69-99: The retry step duplicates the entire SSH script block (the
commands using docker compose pull, docker compose up -d --force-recreate
oracle, the health-check loop that inspects the oracle container and the read
into STATUS IMAGE_ID CREATED HEALTH), so extract that script block into a single
reusable shell script (for example, deploy-oracle.sh) committed to the repo and
replace both SSH-action steps to call that script via the same
appleboy/ssh-action invocation; ensure the new script preserves the set -e, the
for loop health check and the final status/health logging and exit codes so the
behavior stays identical.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1914ac25-9be2-4f10-8b95-83069cae844a
📒 Files selected for processing (1)
.github/workflows/docker.yml
| # Poll healthcheck until container settles (compose.yml: start_period=10s, interval=30s) | ||
| for _ in $(seq 1 60); do | ||
| H=$(docker inspect oracle --format '{{.State.Health.Status}}' 2>/dev/null || echo missing) | ||
| [ "$H" = "healthy" ] && break | ||
| [ "$H" = "missing" ] && { echo "ERROR: oracle container not found"; exit 1; } | ||
| sleep 2 | ||
| done | ||
|
|
||
| read STATUS IMAGE_ID CREATED HEALTH < <(docker inspect oracle \ | ||
| --format '{{.State.Status}} {{slice .Image 7 19}} {{.Created}} {{.State.Health.Status}}') | ||
|
|
||
| if [ "$STATUS" = "running" ] && [ "$HEALTH" = "healthy" ]; then | ||
| echo "oracle deployed: image=$IMAGE_ID created=$CREATED status=$STATUS health=$HEALTH" | ||
| else | ||
| echo "ERROR: oracle deploy failed: status=$STATUS health=$HEALTH image=$IMAGE_ID" | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check default shell for root on typical Linux systems
echo "If KVM2 uses bash as root's shell, process substitution is safe."
echo "Common default shells: bash supports <(), dash/sh do not."Repository: Nkburdick/oracle-app
Length of output: 187
🏁 Script executed:
cat -n .github/workflows/docker.yml | head -100Repository: Nkburdick/oracle-app
Length of output: 4205
🏁 Script executed:
rg "shell|bash|dash" .github/workflows/docker.yml -B 2 -A 2Repository: Nkburdick/oracle-app
Length of output: 46
🌐 Web query:
appleboy/ssh-action default shell bash
💡 Result:
appleboy/ssh-action does not have an input to choose the remote shell (e.g., “use bash”). The action is a composite action whose runner-side steps use shell: bash, but that’s just for running entrypoint.sh on the GitHub runner. [1]
When it connects over SSH, the commands in script: are executed on the remote host using whatever shell SSH uses for that account (typically the user’s login shell / system default), because the action delegates execution to drone-ssh. [2]
If you need Bash specifically, wrap your commands, e.g.:
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USER }}
key: ${{ secrets.KEY }}
script: |
bash -lc 'set -euo pipefail
echo "$BASH_VERSION"
your_command_here
'Sources: [1] [2]
🏁 Script executed:
rg "uses: appleboy/ssh-action" -A 10 .github/workflows/docker.yml | head -30Repository: Nkburdick/oracle-app
Length of output: 725
🏁 Script executed:
sed -n '45,99p' .github/workflows/docker.yml | cat -nRepository: Nkburdick/oracle-app
Length of output: 2834
Use bash explicitly or switch to a portable alternative for reading docker inspect output.
The read ... < <() process substitution syntax on line 59 is bash-specific. Since appleboy/ssh-action executes commands on the remote host using the user's login shell (without forcing bash), this will fail if root's default shell on KVM2 is dash or POSIX sh.
Either add bash -c to the script or use a portable alternative:
Portable alternative using command substitution
- read STATUS IMAGE_ID CREATED HEALTH < <(docker inspect oracle \
- --format '{{.State.Status}} {{slice .Image 7 19}} {{.Created}} {{.State.Health.Status}}')
+ INSPECT_OUT=$(docker inspect oracle \
+ --format '{{.State.Status}} {{slice .Image 7 19}} {{.Created}} {{.State.Health.Status}}')
+ STATUS=$(echo "$INSPECT_OUT" | awk '{print $1}')
+ IMAGE_ID=$(echo "$INSPECT_OUT" | awk '{print $2}')
+ CREATED=$(echo "$INSPECT_OUT" | awk '{print $3}')
+ HEALTH=$(echo "$INSPECT_OUT" | awk '{print $4}')Alternatively, prepend the entire script with bash -c '...your script...' to guarantee bash execution regardless of KVM2's default shell. Note: this same syntax also appears in the "Retry on failure" step (line 91).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Poll healthcheck until container settles (compose.yml: start_period=10s, interval=30s) | |
| for _ in $(seq 1 60); do | |
| H=$(docker inspect oracle --format '{{.State.Health.Status}}' 2>/dev/null || echo missing) | |
| [ "$H" = "healthy" ] && break | |
| [ "$H" = "missing" ] && { echo "ERROR: oracle container not found"; exit 1; } | |
| sleep 2 | |
| done | |
| read STATUS IMAGE_ID CREATED HEALTH < <(docker inspect oracle \ | |
| --format '{{.State.Status}} {{slice .Image 7 19}} {{.Created}} {{.State.Health.Status}}') | |
| if [ "$STATUS" = "running" ] && [ "$HEALTH" = "healthy" ]; then | |
| echo "oracle deployed: image=$IMAGE_ID created=$CREATED status=$STATUS health=$HEALTH" | |
| else | |
| echo "ERROR: oracle deploy failed: status=$STATUS health=$HEALTH image=$IMAGE_ID" | |
| exit 1 | |
| fi | |
| # Poll healthcheck until container settles (compose.yml: start_period=10s, interval=30s) | |
| for _ in $(seq 1 60); do | |
| H=$(docker inspect oracle --format '{{.State.Health.Status}}' 2>/dev/null || echo missing) | |
| [ "$H" = "healthy" ] && break | |
| [ "$H" = "missing" ] && { echo "ERROR: oracle container not found"; exit 1; } | |
| sleep 2 | |
| done | |
| INSPECT_OUT=$(docker inspect oracle \ | |
| --format '{{.State.Status}} {{slice .Image 7 19}} {{.Created}} {{.State.Health.Status}}') | |
| STATUS=$(echo "$INSPECT_OUT" | awk '{print $1}') | |
| IMAGE_ID=$(echo "$INSPECT_OUT" | awk '{print $2}') | |
| CREATED=$(echo "$INSPECT_OUT" | awk '{print $3}') | |
| HEALTH=$(echo "$INSPECT_OUT" | awk '{print $4}') | |
| if [ "$STATUS" = "running" ] && [ "$HEALTH" = "healthy" ]; then | |
| echo "oracle deployed: image=$IMAGE_ID created=$CREATED status=$STATUS health=$HEALTH" | |
| else | |
| echo "ERROR: oracle deploy failed: status=$STATUS health=$HEALTH image=$IMAGE_ID" | |
| exit 1 | |
| fi |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/docker.yml around lines 51 - 67, The script uses bash-only
process substitution in the line starting with "read STATUS IMAGE_ID CREATED
HEALTH < <(docker inspect oracle --format ...)" which will fail under POSIX
shells (e.g., dash); fix it by either forcing bash for the remote execution
(wrap the entire script in bash -c '...') or replace that read/< <(...) usage
with a portable form that captures docker inspect output into a variable and
splits it into STATUS, IMAGE_ID, CREATED, HEALTH using IFS/positional expansion
(and apply the same change for the other occurrence in the "Retry on failure"
step).
Summary
Closes the manual-deploy gap caught 2026-04-30: today's
docker.ymlonly builds + pushes toghcr.io, leaving production on whatever image was last manually pulled. After this PR, every merge tomainauto-deploys.PAI Orch task:
pai-p3-infra-007deploy-to-kvm2job — runs afterbuild-pushsucceedsroot@KVM2, runsdocker compose pull && up -d --force-recreate oracleon/opt/oracle-stackdocker inspect(status=running + health=healthy)Deploy Pennyworthworkflow)Setup already done (no action needed before merge)
oracle-app-deploy-ci(ed25519)root@KVM2:/root/.ssh/authorized_keysSHA256:4eLL4fHWCO...whoami → root,docker compose ps → oracle Up healthy)KVM2_HOSTrepo secret31.220.21.243)KVM2_ROOT_SSH_KEYrepo secretHeads up — first auto-deploy fires on merge
The instant this PR merges, the workflow runs end-to-end (build-push + deploy-to-kvm2). The deploy job will pull the just-built image and recreate the
oraclecontainer on KVM2. Brief blip (a few seconds) on https://oracle.aptoworks.cloud is expected. Traefik + containerrestart: unless-stopped+ healthcheck cover it.If something goes sideways, the manual fallback still works:
/simplify pass applied
Three reviewer agents flagged 6 quality/efficiency issues; all fixed before commit:
sleep 5docker inspectinstead of three separate calls{{slice .Image 7 19}}instead of fragilecut -c8-19STATUS=running && HEALTH=healthycheckSkipped (intentional): digest-gate optimization (over-engineering for daily cadence), removing pull from retry (idempotent + cheap).
Test plan
Docker Build & Pushworkflow run on Actions tabdeploy-to-kvm2job logs showoracle deployed: image=<sha> ... status=running health=healthyssh root@31.220.21.243 'docker inspect oracle --format "{{.Created}}"'curl -I https://oracle.aptoworks.cloud/loginreturnsHTTP/2 200🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes