Skip to content

ci(deploy): auto-deploy oracle-app to KVM2 on merge#26

Merged
Nkburdick merged 1 commit intomainfrom
feat/auto-deploy-kvm2
May 1, 2026
Merged

ci(deploy): auto-deploy oracle-app to KVM2 on merge#26
Nkburdick merged 1 commit intomainfrom
feat/auto-deploy-kvm2

Conversation

@Nkburdick
Copy link
Copy Markdown
Owner

@Nkburdick Nkburdick commented May 1, 2026

Summary

Closes the manual-deploy gap caught 2026-04-30: today's docker.yml only builds + pushes to ghcr.io, leaving production on whatever image was last manually pulled. After this PR, every merge to main auto-deploys.

PAI Orch task: pai-p3-infra-007

  • Adds deploy-to-kvm2 job — runs after build-push succeeds
  • SSHes into root@KVM2, runs docker compose pull && up -d --force-recreate oracle on /opt/oracle-stack
  • Verifies via healthcheck poll + coalesced docker inspect (status=running + health=healthy)
  • One retry on failure (mirrors Pennyworth's Deploy Pennyworth workflow)

Setup already done (no action needed before merge)

Item Status
New SSH keypair oracle-app-deploy-ci (ed25519) Generated locally
Public key added to root@KVM2:/root/.ssh/authorized_keys Done — fingerprint SHA256:4eLL4fHWCO...
Local SSH test with new key Passed (whoami → root, docker compose ps → oracle Up healthy)
KVM2_HOST repo secret Set (31.220.21.243)
KVM2_ROOT_SSH_KEY repo secret Set (private key contents)

Heads 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 oracle container on KVM2. Brief blip (a few seconds) on https://oracle.aptoworks.cloud is expected. Traefik + container restart: unless-stopped + healthcheck cover it.

If something goes sideways, the manual fallback still works:

ssh root@31.220.21.243 'cd /opt/oracle-stack && docker compose pull oracle && docker compose up -d oracle'

/simplify pass applied

Three reviewer agents flagged 6 quality/efficiency issues; all fixed before commit:

  • Healthcheck poll instead of sleep 5
  • Single coalesced docker inspect instead of three separate calls
  • Template {{slice .Image 7 19}} instead of fragile cut -c8-19
  • Explicit STATUS=running && HEALTH=healthy check
  • Retry block now also runs full verification (was silently passing on broken container)
  • Avoided em-dash in error messages (renders oddly in GH Actions logs)

Skipped (intentional): digest-gate optimization (over-engineering for daily cadence), removing pull from retry (idempotent + cheap).

Test plan

  • After merge, watch the Docker Build & Push workflow run on Actions tab
  • Confirm deploy-to-kvm2 job logs show oracle deployed: image=<sha> ... status=running health=healthy
  • Verify container creation timestamp updated: ssh root@31.220.21.243 'docker inspect oracle --format "{{.Created}}"'
  • Smoke test: curl -I https://oracle.aptoworks.cloud/login returns HTTP/2 200

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Chores
    • Implemented automated deployment pipeline with built-in health verification and automatic retry on failure to ensure reliable service updates.

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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

The pull request adds a post-build deployment job to the Docker workflow that connects to KVM2 via SSH, pulls and force-recreates the oracle container using Docker Compose, verifies container health via polling, and includes retry logic if the initial deployment fails.

Changes

Cohort / File(s) Summary
KVM2 Deployment Pipeline
.github/workflows/docker.yml
Added new deploy-to-kvm2 job with SSH connection, Docker Compose pull/recreate operations, bounded health check polling loop, container status inspection, and conditional retry step on deployment failure.

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
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 The Docker dreams of distant clouds,
Where KVM2 waits in misty shrouds,
SSH whispers secrets through the night,
Containers dance and health checks glow bright!
With retry loops we'll never say die,
Our pipeline leaps beyond the sky!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'ci(deploy): auto-deploy oracle-app to KVM2 on merge' directly and clearly summarizes the main change: adding automated deployment to KVM2 on merge via GitHub Actions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/auto-deploy-kvm2

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.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
.github/workflows/docker.yml (1)

69-99: ⚖️ Poor tradeoff

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between 14789cd and f428213.

📒 Files selected for processing (1)
  • .github/workflows/docker.yml

Comment on lines +51 to +67
# 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -100

Repository: Nkburdick/oracle-app

Length of output: 4205


🏁 Script executed:

rg "shell|bash|dash" .github/workflows/docker.yml -B 2 -A 2

Repository: 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 -30

Repository: Nkburdick/oracle-app

Length of output: 725


🏁 Script executed:

sed -n '45,99p' .github/workflows/docker.yml | cat -n

Repository: 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.

Suggested change
# 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).

@Nkburdick Nkburdick merged commit d543513 into main May 1, 2026
2 checks passed
@Nkburdick Nkburdick deleted the feat/auto-deploy-kvm2 branch May 1, 2026 21:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant