Skip to content

feat: enhance Docker deployment with GitHub Container Registry support #20

feat: enhance Docker deployment with GitHub Container Registry support

feat: enhance Docker deployment with GitHub Container Registry support #20

Workflow file for this run

name: "CI tests"
on:
push:
pull_request:
branches: [main]
jobs:
build-matrix:
name: Tests and application run on ${{ matrix.config.name }}
runs-on: ${{ matrix.config.os }}
strategy:
fail-fast: false
matrix:
config:
- {
name: "Windows Latest MinGW", artifact: "Windows-Ninja.tar.xz",
os: windows-latest,
build_type: "Release", cc: "gcc", cxx: "g++",
}
- {
name: "Ubuntu Latest GCC", artifact: "Linux.tar.xz",
os: ubuntu-latest,
build_type: "Release", cc: "gcc", cxx: "g++"
}
steps:
- uses: actions/checkout@v4
- uses: seanmiddleditch/gha-setup-ninja@master
- name: Create CMake cache
shell: bash
run: |
cmake -S . -B cmake-build-release -DCMAKE_BUILD_TYPE=Release -G "Ninja"
- name: Build main target
shell: bash
run: |
cmake --build cmake-build-release --target vox-server
- name: Run program
shell: bash
working-directory: ./cmake-build-release/bin
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
./vox-server.exe --help
else
./vox-server --help
fi
- name: Build tests
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
cmake --build ./cmake-build-release --target vox-server_tests || echo "Built with errors"
else
cmake --build ./cmake-build-release --target vox-server_tests
fi
- name: Run tests
shell: bash
working-directory: ./cmake-build-release/tests
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
./vox-server_tests.exe || echo "Tests failed" # Due to a specific MinGW-related Github Actions issue
else
./vox-server_tests
fi
- name: Build net integration tests
shell: bash
run: |
cmake --build ./cmake-build-release --target vox-server_net_tests
- name: Run net integration tests
shell: bash
working-directory: ./cmake-build-release/tests
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
./vox-server_net_tests.exe || echo "Net tests failed"
else
./vox-server_net_tests
fi
style-check:
name: Code style check with clang-format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install clang-format
run: |
sudo apt-get update && sudo apt-get -y install clang-format
- name: Check code style
shell: bash
run: |
mapfile -t files < <(git ls-files '*.c' '*.cpp' '*.h' '*.hpp')
if [ "${#files[@]}" -eq 0 ]; then
echo "No C/C++ files to check."
exit 0
fi
clang-format --dry-run --Werror "${files[@]}" 2>format_output.txt || {
cat format_output.txt
exit 1
}
- name: Comment on style issues
if: failure() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const { execSync } = require('child_process');
try {
// Get list of files that need formatting
const rawFiles = execSync('git ls-files "*.c" "*.cpp" "*.h" "*.hpp"', { encoding: 'utf8' }).trim();
if (!rawFiles) {
console.log('No files require formatting checks.');
return;
}
const files = rawFiles.split('\n');
let comment = '## 🎨 Code Style Issues Found\n\n';
comment += 'The following files have formatting issues:\n\n';
let hasIssues = false;
for (const file of files) {
try {
const result = execSync(`clang-format --dry-run --Werror "${file}" 2>&1`, { encoding: 'utf8' });
} catch (error) {
comment += `- \`${file}\`: Formatting issues detected\n`;
hasIssues = true;
}
}
if (!hasIssues) {
comment += 'No files with formatting issues were detected.';
} else {
comment += '\nPlease run `clang-format -i <file>` to fix formatting issues.';
}
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
} catch (error) {
console.log('Could not create comment:', error.message);
}
code-quality-check:
name: Code quality check with clang-tidy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install clang-tidy and GCC 13
run: |
sudo apt-get update
# Install a C++23-capable toolchain and clang-tidy-19 for better C++23 support
sudo apt-get -y install clang-19 clang-tidy-19
- name: Install Boost
run: |
sudo apt update
sudo apt install libboost-all-dev
- name: Create CMake cache
run: |
cmake -S . -B cmake-build-tidy \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
- name: Run clang-tidy
shell: bash
run: |
mapfile -t files < <(git ls-files '*.c' '*.cpp')
if [ "${#files[@]}" -eq 0 ]; then
echo "No C/C++ files to analyze."
echo "" > tidy_output.txt
exit 0
fi
echo "Running clang-tidy-19 on ${#files[@]} files..."
# Use clang-tidy-19 for better C++23 support and --extra-arg-before to ensure C++23 standard is set before other flags
clang-tidy-19 "${files[@]}" -p cmake-build-tidy --format-style=file > tidy_output.txt 2>&1 || true
# Ensure file exists and is readable
if [ ! -f tidy_output.txt ]; then
echo "" > tidy_output.txt
fi
- name: Count warnings and errors
id: count_issues
run: |
# Count errors and warnings - handle empty file case
if [ ! -s tidy_output.txt ]; then
errors=0
warnings=0
else
errors=$(grep -c "error:" tidy_output.txt 2>/dev/null || echo "0")
warnings=$(grep -c "warning:" tidy_output.txt 2>/dev/null || echo "0")
fi
# Ensure we have clean integer values
errors=$(echo "$errors" | tr -d '\n' | head -c 10)
warnings=$(echo "$warnings" | tr -d '\n' | head -c 10)
# Default to 0 if empty or non-numeric
errors=${errors:-0}
warnings=${warnings:-0}
echo "errors=$errors" >> $GITHUB_OUTPUT
echo "warnings=$warnings" >> $GITHUB_OUTPUT
echo "Found $errors errors and $warnings warnings"
if [ "$errors" -eq 0 ] && [ "$warnings" -le 3 ]; then
echo "clang-tidy found $warnings warnings"
cat tidy_output.txt
exit 0
fi
# Fail if more than 3 warnings or any errors
if [ "$errors" -gt 0 ] || [ "$warnings" -gt 3 ]; then
echo "clang-tidy found $errors errors and $warnings warnings"
cat tidy_output.txt
exit 1
fi
- name: Comment on quality issues
if: failure() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
try {
let comment = '## 🔍 Code Quality Issues Found\n\n';
if (fs.existsSync('tidy_output.txt')) {
const output = fs.readFileSync('tidy_output.txt', 'utf8');
const lines = output.split('\n');
let currentFile = '';
let hasIssues = false;
for (const line of lines) {
if (line.includes('error:') || line.includes('warning:')) {
const parts = line.split(':');
if (parts.length >= 4) {
const file = parts[0];
const lineNum = parts[1];
const message = parts.slice(3).join(':').trim();
if (file !== currentFile) {
if (hasIssues) comment += '\n';
comment += `### \`${file}\`\n\n`;
currentFile = file;
hasIssues = true;
}
const issueType = line.includes('error:') ? '❌ Error' : '⚠️ Warning';
comment += `- **Line ${lineNum}**: ${issueType} - ${message}\n`;
}
}
}
if (!hasIssues) {
comment += 'No specific issues found in the output.';
}
} else {
comment += 'Could not read clang-tidy output.';
}
comment += '\n\nPlease review and fix the issues above.';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
} catch (error) {
console.log('Could not create comment:', error.message);
}
# Build image on GitHub-hosted runner, push to GHCR (no compile on the VPS).
docker-publish:
name: Build and push image (GHCR)
runs-on: ubuntu-latest
needs: [build-matrix, style-check, code-quality-check]
if: |
success() &&
(
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(github.event_name == 'pull_request' && github.base_ref == 'main' && github.event.pull_request.head.repo.full_name == github.repository)
)
permissions:
contents: read
packages: write
concurrency:
group: docker-publish
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- name: Image name (lowercase for GHCR)
id: meta
run: |
echo "repository_lower=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push (image tagged with commit SHA)
uses: docker/build-push-action@v6
with:
context: .
file: ./deploy/Dockerfile
push: true
tags: ghcr.io/${{ steps.meta.outputs.repository_lower }}:${{ github.sha }}
# Avoid overwriting :latest from PR builds — only main tracks latest.
- name: Tag latest (main branch only)
if: github.ref == 'refs/heads/main'
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker pull "ghcr.io/${{ steps.meta.outputs.repository_lower }}:${{ github.sha }}"
docker tag "ghcr.io/${{ steps.meta.outputs.repository_lower }}:${{ github.sha }}" \
"ghcr.io/${{ steps.meta.outputs.repository_lower }}:latest"
docker push "ghcr.io/${{ steps.meta.outputs.repository_lower }}:latest"
# Pull prebuilt image on the server (git pull only updates nginx/compose; app comes from GHCR).
deploy-server:
name: Deploy (pull image + compose up)
runs-on: ubuntu-latest
needs: [docker-publish]
if: |
success() &&
(
(github.event_name == 'push' && github.ref == 'refs/heads/main') ||
(github.event_name == 'pull_request' && github.base_ref == 'main' && github.event.pull_request.head.repo.full_name == github.repository)
)
concurrency:
group: deploy-production
cancel-in-progress: false
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.2.0
env:
VOX_ADMIN_TOKEN: ${{ secrets.VOX_ADMIN_TOKEN }}
GHCR_READ_TOKEN: ${{ secrets.GHCR_READ_TOKEN }}
GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
GITHUB_REPO_OWNER: ${{ github.repository_owner }}
with:
host: messenger.bialger.com
username: ${{ secrets.SERVER_LOGIN }}
password: ${{ secrets.SERVER_PASSWORD }}
port: 22
command_timeout: 30m
envs: VOX_ADMIN_TOKEN,GHCR_READ_TOKEN,GHCR_USERNAME,GITHUB_REPO_OWNER
script_stop: true
script: |
set -euo pipefail
BRANCH="${{ github.event_name == 'pull_request' && github.head_ref || github.ref_name }}"
REPO_LC=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
IMAGE="ghcr.io/${REPO_LC}:${{ github.sha }}"
cd /opt/vox-server
git fetch origin "${BRANCH}"
git checkout "${BRANCH}"
git pull --ff-only origin "${BRANCH}"
cd deploy
{
echo "VOX_IMAGE=${IMAGE}"
echo "VOX_ADMIN_TOKEN=${VOX_ADMIN_TOKEN:-}"
} > .env
if [ -n "${GHCR_READ_TOKEN:-}" ]; then
U="${GHCR_USERNAME:-${GITHUB_REPO_OWNER}}"
echo "${GHCR_READ_TOKEN}" | docker login ghcr.io -u "${U}" --password-stdin
fi
docker compose pull vox-server
docker compose up -d