Two independent GitHub Actions pipelines — one per repo — each gating on tests before a Docker image reaches GHCR, with the frontend pipeline going one step further into an SSH deploy + healthcheck.
# amigos_unite_api / .github/workflows/backend-ci.yml
name: Backend CI
on:
pull_request:
branches: [main]
paths:
- "app/**"
- "config/**"
- "db/**"
- "spec/**"
- "Gemfile"
- "Gemfile.lock"
jobs:
test:
name: backend-ci / test
runs-on: ubuntu-24.04
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: amigos_unite_api_test
ports:
- "5432:5432"
options: >-
--health-cmd="pg_isready -U postgres -d amigos_unite_api_test"
--health-interval=10s
--health-timeout=5s
--health-retries=10
env:
RAILS_ENV: test
DATABASE_URL: postgres://postgres:postgres@localhost:5432/amigos_unite_api_test
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY_TEST }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install OS deps
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
libpq-dev postgresql-client pkg-config \
libvips libvips-dev libvips-tools
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2.2"
bundler: "2.5.17"
bundler-cache: true
- name: Materialize test credentials key (validate shape)
run: |
KEY="$(printf '%s' "${RAILS_MASTER_KEY:-}" | tr -d '\r\n')"
if [[ ! "$KEY" =~ ^[0-9A-Fa-f]{32}$ ]]; then
echo "ERROR: RAILS_MASTER_KEY_TEST must be exactly 32 hex characters." >&2
exit 1
fi
mkdir -p config/credentials
printf '%s' "$KEY" > config/credentials/test.key
chmod 0400 config/credentials/test.key
- name: Prepare database (CI-safe - no seeds)
run: bin/rails db:drop db:create db:schema:load
- name: Run RSpec suite
run: bundle exec rspec
Path-filtered so it only runs on backend changes; validates the test credentials key's shape before Rails ever touches it, to fail with a clear message instead of a cryptic decrypt error.
# amigos_unite_app / .github/workflows/ghcr.yml
# lines 1–80 of 140 (test → build → push)
name: Build, Push, Deploy (GHCR) - WEB
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch: {}
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: sagacic-tim/amigos_unite_app
jobs:
test:
name: Lint, typecheck & build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Lint and typecheck
run: npm test
- name: Build
run: npm run build
env:
VITE_GOOGLE_MAPS_API_KEY: ${{ secrets.VITE_GOOGLE_MAPS_API_KEY }}
build_and_push:
name: Build & push Docker image
needs: test
runs-on: ubuntu-latest
if: github.event_name != 'pull_request'
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata (tags/labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=main
type=sha,format=short
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
VITE_GOOGLE_MAPS_API_KEY=${{ secrets.VITE_GOOGLE_MAPS_API_KEY }}
Deploy stage (not shown — SSH + healthcheck loop against the VPS) only runs after both this test job and the image push succeed.