chore: initial hub repo structure
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
name: Deploy Docker to VPS
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['Build and Push Docker Images']
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'infra/**'
|
||||
- '.github/workflows/deploy-docker.yml'
|
||||
- '.github/workflows/docker-build-push.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
# Prevent multiple deployments from running simultaneously
|
||||
concurrency:
|
||||
group: deploy-vps
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.workflow_run.conclusion == 'success'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
submodules: false
|
||||
|
||||
- name: Deploy to VPS
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||
VPS_USER: ${{ secrets.VPS_USER }}
|
||||
VPS_TARGET_DIR: ${{ secrets.VPS_TARGET_DIR }}
|
||||
ENV_FILE_PRODUCTION: ${{ secrets.ENV_FILE_PRODUCTION }}
|
||||
GHCR_USERNAME: ${{ github.actor }}
|
||||
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Deploy event: ${{ github.event_name }}"
|
||||
echo "Deploy ref: ${{ github.ref }}"
|
||||
echo "Deploy sha: ${{ github.sha }}"
|
||||
|
||||
if [ -z "${SSH_PRIVATE_KEY:-}" ] || [ -z "${VPS_HOST:-}" ] || [ -z "${VPS_USER:-}" ] || [ -z "${VPS_TARGET_DIR:-}" ]; then
|
||||
echo "❌ Deployment secrets are not fully configured. Please set SSH_PRIVATE_KEY, VPS_HOST, VPS_USER, and VPS_TARGET_DIR."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p ~/.ssh
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H -t ed25519,rsa "$VPS_HOST" >> ~/.ssh/known_hosts
|
||||
|
||||
# Use SSH multiplexing for faster subsequent commands
|
||||
SSH_OPTS=(-o ControlMaster=auto -o ControlPath=/tmp/ssh-%r@%h:%p -o ControlPersist=600 -o StrictHostKeyChecking=yes)
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "mkdir -p $VPS_TARGET_DIR && mkdir -p $VPS_TARGET_DIR/infra/compose"
|
||||
echo "$ENV_FILE_PRODUCTION" > .env.prod
|
||||
scp "${SSH_OPTS[@]}" .env.prod "$VPS_USER@$VPS_HOST:$VPS_TARGET_DIR/.env"
|
||||
|
||||
echo "🔐 Logging in to GitHub Container Registry..."
|
||||
printf '%s' "$GHCR_TOKEN" | ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "docker login ghcr.io -u '$GHCR_USERNAME' --password-stdin"
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "export VPS_TARGET_DIR=$VPS_TARGET_DIR; bash -s" <<'EOF'
|
||||
set -euo pipefail
|
||||
cd "$VPS_TARGET_DIR"
|
||||
|
||||
# Ensure shared network exists
|
||||
docker network inspect app-shared-net >/dev/null 2>&1 || docker network create app-shared-net
|
||||
|
||||
echo "🔄 Synchronizing repository..."
|
||||
if [ ! -d ".git" ]; then
|
||||
echo "Initializing git repository..."
|
||||
git init
|
||||
git remote add origin https://github.com/asepharyana/asepharyana-hub.git
|
||||
fi
|
||||
git fetch origin main --depth=1 || true
|
||||
|
||||
# Detect changed files before resetting
|
||||
ALL_COMPOSE_FILES="infra/compose/traefik.yml infra/compose/shared.yml infra/compose/scraper.yml infra/compose/elysia.yml infra/compose/react.yml infra/compose/rust-auth.yml"
|
||||
TRAEFIK_DYNAMIC_DIR="infra/traefik/dynamic"
|
||||
|
||||
if git rev-parse HEAD >/dev/null 2>&1; then
|
||||
BEFORE_REV=$(git rev-parse HEAD)
|
||||
git reset --hard FETCH_HEAD
|
||||
AFTER_REV=$(git rev-parse HEAD)
|
||||
|
||||
if [ "$BEFORE_REV" = "$AFTER_REV" ]; then
|
||||
echo "ℹ️ No new commits detected. Using full file list for safety."
|
||||
TARGET_COMPOSE=""
|
||||
else
|
||||
CHANGED=$(git diff --name-only "$BEFORE_REV" "$AFTER_REV" || true)
|
||||
echo "📄 Changed files:"
|
||||
echo "$CHANGED"
|
||||
|
||||
# Detect compose stack changes
|
||||
CHANGED_COMPOSE=$(echo "$CHANGED" | grep '^infra/compose/.*\.yml$' || true)
|
||||
TARGET_COMPOSE=""
|
||||
for f in $CHANGED_COMPOSE; do
|
||||
case " $ALL_COMPOSE_FILES " in
|
||||
*" $f "*) TARGET_COMPOSE="$TARGET_COMPOSE $f" ;;
|
||||
esac
|
||||
done
|
||||
TARGET_COMPOSE=$(printf '%s' "$TARGET_COMPOSE" | xargs || true)
|
||||
if [ -n "$TARGET_COMPOSE" ]; then
|
||||
echo "🎯 Detected compose stack changes in: $TARGET_COMPOSE"
|
||||
else
|
||||
echo "ℹ️ No stack compose files changed."
|
||||
fi
|
||||
|
||||
# Detect Traefik dynamic config changes
|
||||
CHANGED_TRAEFIK=$(echo "$CHANGED" | grep "^$TRAEFIK_DYNAMIC_DIR/" || true)
|
||||
if [ -n "$CHANGED_TRAEFIK" ]; then
|
||||
echo "🎯 Detected Traefik dynamic config changes:"
|
||||
echo "$CHANGED_TRAEFIK"
|
||||
RELOAD_TRAEFIK="true"
|
||||
else
|
||||
echo "ℹ️ No Traefik dynamic config changes."
|
||||
fi
|
||||
|
||||
# Detect infra file changes (Dockerfiles, config, traefik static)
|
||||
CHANGED_INFRA=$(echo "$CHANGED" | grep '^infra/' | grep -v '^infra/compose/' || true)
|
||||
if [ -n "$CHANGED_INFRA" ]; then
|
||||
echo "📦 Detected other infra file changes:"
|
||||
echo "$CHANGED_INFRA"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
git reset --hard FETCH_HEAD
|
||||
TARGET_COMPOSE=""
|
||||
fi
|
||||
|
||||
# Submodule update removed for faster VPS deployments
|
||||
|
||||
if command -v "docker" >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose >/dev/null 2>&1; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
echo "❌ docker compose is not installed on the remote host."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Construct compose arguments
|
||||
docker rm -f imphenbot-app || true
|
||||
|
||||
if [ -n "$TARGET_COMPOSE" ]; then
|
||||
COMPOSE_ARGS=""
|
||||
for f in $TARGET_COMPOSE; do
|
||||
if [ -f "$f" ]; then
|
||||
COMPOSE_ARGS="$COMPOSE_ARGS -f $f"
|
||||
fi
|
||||
done
|
||||
UP_FLAGS="-d" # No --remove-orphans for selective updates to avoid killing other services
|
||||
else
|
||||
echo "🚀 Performing full deployment of all services..."
|
||||
COMPOSE_ARGS=""
|
||||
for f in $ALL_COMPOSE_FILES; do
|
||||
COMPOSE_ARGS="$COMPOSE_ARGS -f $f"
|
||||
done
|
||||
UP_FLAGS="-d --remove-orphans"
|
||||
fi
|
||||
|
||||
echo "📥 Pulling images for target services..."
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
PULL_SUCCESS=false
|
||||
# Retry pull up to 3 times to handle transient Docker attestation lease errors
|
||||
for attempt in 1 2 3; do
|
||||
echo "Pull attempt $attempt/3..."
|
||||
if $COMPOSE_CMD $COMPOSE_ARGS --env-file .env pull; then
|
||||
echo "✅ Pull succeeded on attempt $attempt"
|
||||
PULL_SUCCESS=true
|
||||
break
|
||||
else
|
||||
echo "⚠️ Pull attempt $attempt failed. Retrying in 5s..."
|
||||
sleep 5
|
||||
fi
|
||||
done
|
||||
if [ "$PULL_SUCCESS" != "true" ]; then
|
||||
echo "❌ Failed to pull images after 3 attempts."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🧹 Clearing Git locks..."
|
||||
rm -f .git/shallow.lock || true
|
||||
|
||||
# Cooldown to allow daemon to settle (optional but kept for safety)
|
||||
# echo "⏳ Waiting for Docker daemon to settle..."
|
||||
# sleep 2
|
||||
|
||||
echo "🧹 Removing stale target containers by container_name..."
|
||||
# Extract all explicitly defined container_names from compose files and remove them to prevent conflicts
|
||||
if [ -n "$TARGET_COMPOSE" ]; then
|
||||
for f in $TARGET_COMPOSE; do
|
||||
if [ -f "$f" ]; then
|
||||
grep "container_name:" "$f" | awk '{print $2}' | while read -r cname; do
|
||||
docker rm -f "$cname" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
done
|
||||
else
|
||||
for f in $ALL_COMPOSE_FILES; do
|
||||
if [ -f "$f" ]; then
|
||||
grep "container_name:" "$f" | awk '{print $2}' | while read -r cname; do
|
||||
docker rm -f "$cname" >/dev/null 2>&1 || true
|
||||
done
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "🆙 Starting services..."
|
||||
echo "🔍 Debug: Current docker containers:"
|
||||
docker ps -a
|
||||
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up $UP_FLAGS
|
||||
|
||||
# ── Traefik reload ──
|
||||
if [ "${RELOAD_TRAEFIK:-false}" = "true" ]; then
|
||||
echo "🔄 Traefik dynamic config changed — reloading Traefik..."
|
||||
# Traefik watches the dynamic config dir (providers.file.watch=true),
|
||||
# but send SIGHUP as insurance
|
||||
docker kill --signal HUP traefik 2>/dev/null || docker exec traefik kill -HUP 1 2>/dev/null || true
|
||||
echo "✅ Traefik reload signal sent"
|
||||
fi
|
||||
EOF
|
||||
|
||||
# - name: Restart coolify-proxy (delay 1 min)
|
||||
# env:
|
||||
# SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
# VPS_HOST: ${{ secrets.VPS_HOST }}
|
||||
# VPS_USER: ${{ secrets.VPS_USER }}
|
||||
# run: |
|
||||
# mkdir -p ~/.ssh
|
||||
# echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
# chmod 600 ~/.ssh/id_rsa
|
||||
# ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts
|
||||
#
|
||||
# echo "⏳ Waiting 60s for containers to settle before restarting proxy..."
|
||||
# sleep 60
|
||||
#
|
||||
# echo "🔄 Restarting coolify-proxy..."
|
||||
# ssh "${VPS_USER}@${VPS_HOST}" "docker restart coolify-proxy"
|
||||
# echo "✅ coolify-proxy restarted."
|
||||
@@ -0,0 +1,331 @@
|
||||
name: Build and Push Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apps/**'
|
||||
- 'packages/**'
|
||||
- '.github/workflows/docker-build-push.yml'
|
||||
- 'infra/**'
|
||||
- '!infra/compose/**'
|
||||
repository_dispatch:
|
||||
types: [submodule-updated]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME_PREFIX: asepharyana/asepharyana-hub
|
||||
|
||||
jobs:
|
||||
# ──────────────────────────────────────────────
|
||||
# Phase 1: Detect which services have changed
|
||||
# ──────────────────────────────────────────────
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
scraper-api: ${{ steps.filter.outputs['scraper-api'] == 'true' || steps.dispatch.outputs['scraper-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
elysia-api: ${{ steps.filter.outputs['elysia-api'] == 'true' || steps.dispatch.outputs['elysia-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
react-web: ${{ steps.filter.outputs['react-web'] == 'true' || steps.dispatch.outputs['react-web'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
rust-auth: ${{ steps.filter.outputs['rust-auth'] == 'true' || steps.dispatch.outputs['rust-auth'] == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Detect changed services
|
||||
id: filter
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
BEFORE: ${{ github.event.before }}
|
||||
AFTER: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${BEFORE:-}" ] || [[ "$BEFORE" =~ ^0+$ ]]; then
|
||||
CHANGED_FILES=$(git ls-files)
|
||||
else
|
||||
git fetch --no-tags --depth=2 origin "$BEFORE" || true
|
||||
CHANGED_FILES=$(git diff --name-only "$BEFORE" "$AFTER")
|
||||
fi
|
||||
|
||||
changed() {
|
||||
printf '%s\n' "$CHANGED_FILES" | grep -Eq "$1" && echo true || echo false
|
||||
}
|
||||
|
||||
echo "scraper-api=$(changed '^(apps/scraper(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/scraper\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||
echo "elysia-api=$(changed '^(apps/elysia(/|$)|packages(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/elysia\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||
echo "react-web=$(changed '^(apps/react(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/react\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
|
||||
echo "rust-auth=$(changed '^(apps/rust-auth(/|$)|infra/docker/rust\.Dockerfile$|\.github/workflows/docker-build-push\.yml$|\.gitmodules$)')" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Parse repository_dispatch payload
|
||||
id: dispatch
|
||||
if: github.event_name == 'repository_dispatch'
|
||||
env:
|
||||
SERVICE: ${{ github.event.client_payload.service }}
|
||||
SHA: ${{ github.event.client_payload.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${SERVICE:-}" ]; then
|
||||
echo "::error::repository_dispatch payload missing service"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${SHA:-}" ]; then
|
||||
echo "::error::repository_dispatch payload missing sha"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$SERVICE" in
|
||||
scraper-api|elysia-api|react-web|rust-auth) ;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if ! [[ "$SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::Invalid sha '$SHA'. Expected 40 hex characters"; exit 1; fi
|
||||
|
||||
SERVICES=(scraper-api elysia-api react-web rust-auth)
|
||||
for svc in "${SERVICES[@]}"; do
|
||||
if [ "$SERVICE" = "$svc" ]; then
|
||||
echo "${svc}=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "${svc}=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Set matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
SERVICES=()
|
||||
add_service() {
|
||||
SERVICES+=("{\"id\":\"$1\",\"target\":\"$2\",\"path\":\"$3\"}")
|
||||
}
|
||||
|
||||
if [ "${{ steps.filter.outputs['scraper-api'] == 'true' || steps.dispatch.outputs['scraper-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "scraper-api" "docker-scraper" "apps/scraper"; fi
|
||||
if [ "${{ steps.filter.outputs['elysia-api'] == 'true' || steps.dispatch.outputs['elysia-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "elysia-api" "docker-elysia" "apps/elysia"; fi
|
||||
if [ "${{ steps.filter.outputs['react-web'] == 'true' || steps.dispatch.outputs['react-web'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "react-web" "docker-react" "apps/react"; fi
|
||||
if [ "${{ steps.filter.outputs['rust-auth'] == 'true' || steps.dispatch.outputs['rust-auth'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "rust-auth" "docker-rust-auth" "apps/rust-auth"; fi
|
||||
|
||||
JSON_ARRAY="[$(IFS=,; echo "${SERVICES[*]}")]"
|
||||
echo "matrix=$JSON_ARRAY" >> $GITHUB_OUTPUT
|
||||
|
||||
wait-submodule-ref:
|
||||
needs: [changes]
|
||||
if: github.event_name == 'repository_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Wait for submodule ref
|
||||
env:
|
||||
SERVICE: ${{ github.event.client_payload.service }}
|
||||
SHA: ${{ github.event.client_payload.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
case "$SERVICE" in
|
||||
"scraper-api") REPO="https://github.com/asepharyana/asepharyana-hub-scraper.git" ;;
|
||||
"elysia-api") REPO="https://github.com/asepharyana/asepharyana-hub-elysia.git" ;;
|
||||
"react-web") REPO="https://github.com/asepharyana/asepharyana-hub-react.git" ;;
|
||||
"rust-auth") REPO="https://github.com/asepharyana/asepharyana-hub-rust-auth.git" ;;
|
||||
*)
|
||||
echo "::error::Unsupported service '$SERVICE'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Waiting for $SERVICE commit $SHA in $REPO"
|
||||
TMPDIR=$(mktemp -d)
|
||||
git init "$TMPDIR/probe" >/dev/null
|
||||
git -C "$TMPDIR/probe" remote add origin "$REPO"
|
||||
for attempt in {1..30}; do
|
||||
if git -C "$TMPDIR/probe" fetch --depth=1 origin "$SHA" >/dev/null 2>&1; then
|
||||
echo "Submodule commit $SHA is fetchable for $SERVICE"
|
||||
rm -rf "$TMPDIR"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $attempt/30: $SHA not fetchable yet; waiting 10s"
|
||||
sleep 10
|
||||
done
|
||||
rm -rf "$TMPDIR"
|
||||
|
||||
echo "::error::Submodule commit $SHA for $SERVICE was not fetchable after 300s"
|
||||
exit 1
|
||||
|
||||
# ─────────────────────────────────────────────────
|
||||
# Phase 2: Build and Push Images (Matrix)
|
||||
# ─────────────────────────────────────────────────
|
||||
build:
|
||||
needs: [changes, wait-submodule-ref]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJson(needs.changes.outputs.matrix) }}
|
||||
if: |
|
||||
always() &&
|
||||
needs.changes.result == 'success' &&
|
||||
(needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') &&
|
||||
needs.changes.outputs.matrix != '[]'
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Sync submodule locally
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
DISPATCH_SHA: ${{ github.event.client_payload.sha }}
|
||||
SUBMODULE_PATH: ${{ matrix.path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git submodule update --init --recursive "$SUBMODULE_PATH"
|
||||
if [ "$EVENT_NAME" = "repository_dispatch" ] && [ -n "${DISPATCH_SHA:-}" ]; then
|
||||
cd "$SUBMODULE_PATH"
|
||||
git fetch origin "$DISPATCH_SHA"
|
||||
git checkout "$DISPATCH_SHA"
|
||||
fi
|
||||
|
||||
- uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
run: |
|
||||
SVC_NAME="${{ matrix.id }}"
|
||||
SHORT=${GITHUB_SHA:0:7}
|
||||
echo "image=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "tags=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:latest,${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:sha-${SHORT}" >> $GITHUB_OUTPUT
|
||||
echo "cache-registry=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:buildcache" >> $GITHUB_OUTPUT
|
||||
case "$SVC_NAME" in
|
||||
"scraper-api") echo "dockerfile=infra/docker/scraper.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||
"elysia-api") echo "dockerfile=infra/docker/elysia.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||
"react-web") echo "dockerfile=infra/docker/react.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||
"rust-auth") echo "dockerfile=infra/docker/rust.Dockerfile" >> $GITHUB_OUTPUT ;;
|
||||
esac
|
||||
|
||||
- name: Build and Push Docker image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ${{ steps.meta.outputs.dockerfile }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
COMMIT_COUNT=${{ env.NR_COMMIT_COUNT || github.run_number }}
|
||||
COMMIT_SHA=${{ env.NR_COMMIT_SHA || github.sha }}
|
||||
cache-from: type=registry,ref=${{ steps.meta.outputs['cache-registry'] }}
|
||||
cache-to: type=registry,ref=${{ steps.meta.outputs['cache-registry'] }},mode=max
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Phase 3: Update Manifests and Submodule Refs
|
||||
# ──────────────────────────────────────────────
|
||||
update-manifest:
|
||||
needs: [changes, wait-submodule-ref, build]
|
||||
if: |
|
||||
always() &&
|
||||
needs.changes.result == 'success' &&
|
||||
(needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') &&
|
||||
(needs.build.result == 'success' || needs.build.result == 'skipped')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
ref: main
|
||||
|
||||
- name: Update tags and submodules
|
||||
run: |
|
||||
SHORT_SHA=${GITHUB_SHA:0:7}
|
||||
TAG="sha-$SHORT_SHA"
|
||||
CHANGED=false
|
||||
|
||||
declare -A SERVICES
|
||||
SERVICES["scraper-api"]="scraper.yml"
|
||||
SERVICES["elysia-api"]="elysia.yml"
|
||||
SERVICES["react-web"]="react.yml"
|
||||
SERVICES["rust-auth"]="rust-auth.yml"
|
||||
|
||||
declare -A PATHS
|
||||
PATHS["scraper-api"]="apps/scraper"
|
||||
PATHS["elysia-api"]="apps/elysia"
|
||||
PATHS["react-web"]="apps/react"
|
||||
PATHS["rust-auth"]="apps/rust-auth"
|
||||
|
||||
# Use git config for possible commits
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
for id in "${!SERVICES[@]}"; do
|
||||
SHOULD_HAVE_RUN=false
|
||||
if [ "${{ needs.changes.outputs['scraper-api'] }}" == "true" ] && [ "$id" == "scraper-api" ]; then SHOULD_HAVE_RUN=true; fi
|
||||
if [ "${{ needs.changes.outputs['elysia-api'] }}" == "true" ] && [ "$id" == "elysia-api" ]; then SHOULD_HAVE_RUN=true; fi
|
||||
if [ "${{ needs.changes.outputs['react-web'] }}" == "true" ] && [ "$id" == "react-web" ]; then SHOULD_HAVE_RUN=true; fi
|
||||
if [ "${{ needs.changes.outputs['rust-auth'] }}" == "true" ] && [ "$id" == "rust-auth" ]; then SHOULD_HAVE_RUN=true; fi
|
||||
|
||||
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then SHOULD_HAVE_RUN=true; fi
|
||||
|
||||
if [ "$SHOULD_HAVE_RUN" == "true" ]; then
|
||||
COMPOSE_FILE="infra/compose/${SERVICES[$id]}"
|
||||
if [ -f "$COMPOSE_FILE" ]; then
|
||||
echo "Updating $COMPOSE_FILE to $TAG"
|
||||
sed -i "s|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_PREFIX }}/$id:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_PREFIX }}/$id:$TAG|g" "$COMPOSE_FILE"
|
||||
git add "$COMPOSE_FILE"
|
||||
CHANGED=true
|
||||
fi
|
||||
|
||||
# If it's a repository_dispatch for this specific service, update its submodule pointer
|
||||
if [ "${{ github.event_name }}" == "repository_dispatch" ] && [ "${{ github.event.client_payload.service }}" == "$id" ]; then
|
||||
SHA_DISPATCH="${{ github.event.client_payload.sha }}"
|
||||
SUB_PATH="${PATHS[$id]}"
|
||||
if [ -n "$SHA_DISPATCH" ]; then
|
||||
echo "Updating submodule $SUB_PATH to $SHA_DISPATCH"
|
||||
git submodule update --init "$SUB_PATH"
|
||||
git -C "$SUB_PATH" fetch origin "$SHA_DISPATCH"
|
||||
git -C "$SUB_PATH" checkout "$SHA_DISPATCH"
|
||||
git add "$SUB_PATH"
|
||||
CHANGED=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$CHANGED" == "true" ]; then
|
||||
git commit -m "chore: update manifests and submodules [skip ci]"
|
||||
|
||||
for attempt in {1..3}; do
|
||||
if git pull --rebase origin main && git push origin main; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Manifest push attempt $attempt/3 failed; retrying"
|
||||
git rebase --abort || true
|
||||
git pull --rebase origin main || true
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "::error::Failed to push manifest update after 3 attempts"
|
||||
exit 1
|
||||
else
|
||||
echo "No changes detected."
|
||||
fi
|
||||
@@ -0,0 +1,29 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'apps/*/src/**/*.ts'
|
||||
- 'apps/*/src/**/*.tsx'
|
||||
- 'apps/*/eslint.config.mjs'
|
||||
- 'eslint.config.mjs'
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'apps/*/src/**/*.ts'
|
||||
- 'apps/*/src/**/*.tsx'
|
||||
- 'eslint.config.mjs'
|
||||
|
||||
jobs:
|
||||
eslint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: npm install -g eslint @antfu/eslint-config
|
||||
- run: eslint . --no-error-on-unmatched-pattern || echo "Lint check completed (best-effort)"
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Security
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # Every Monday
|
||||
|
||||
jobs:
|
||||
codeql:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
- uses: github/codeql-action/analyze@v3
|
||||
@@ -0,0 +1,22 @@
|
||||
name: TypeCheck
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'apps/**/*.ts'
|
||||
- 'apps/**/*.tsx'
|
||||
- 'tsconfig.base.json'
|
||||
|
||||
jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: |
|
||||
cd apps/react && npm install && npx tsc --noEmit || echo "TypeScript check completed"
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Update Submodule Pointer
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [submodule-updated]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Update submodule pointer
|
||||
env:
|
||||
SERVICE: ${{ github.event.client_payload.service }}
|
||||
SHA: ${{ github.event.client_payload.sha }}
|
||||
run: |
|
||||
echo "Updating ${SERVICE} to ${SHA}"
|
||||
git submodule update --init "apps/${SERVICE}"
|
||||
cd "apps/${SERVICE}"
|
||||
# unshallow → full fetch so we get tree objects for the target SHA
|
||||
git fetch --depth=1000 origin master
|
||||
git checkout "${SHA}"
|
||||
cd "${GITHUB_WORKSPACE}"
|
||||
git add "apps/${SERVICE}"
|
||||
git diff --cached --quiet && exit 0
|
||||
git config user.name "monrepo-bot"
|
||||
git config user.email "monrepo-bot@users.noreply.github.com"
|
||||
git commit -m "chore: update ${SERVICE} to ${SHA:0:12}"
|
||||
git push
|
||||
Reference in New Issue
Block a user