426 lines
14 KiB
YAML
426 lines
14 KiB
YAML
name: Gitea Fast Deploy
|
|
|
|
on:
|
|
push:
|
|
branches: [main, master, develop]
|
|
|
|
concurrency:
|
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
HARBOR_HOST_OVERRIDE: ${{ secrets.HARBOR_CN_HOST }}
|
|
HARBOR_PROJECT_OVERRIDE: ${{ secrets.HARBOR_PROJECT }}
|
|
IMAGE_NAME_OVERRIDE: ${{ secrets.IMAGE_NAME }}
|
|
|
|
jobs:
|
|
test:
|
|
name: Test Backend
|
|
if: github.event_name == 'pull_request'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 20
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
env:
|
|
POSTGRES_USER: wrongbook
|
|
POSTGRES_PASSWORD: wrongbook
|
|
POSTGRES_DB: wrongbook_test
|
|
ports:
|
|
- 5433:5432
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
ports:
|
|
- 6380:6379
|
|
options: >-
|
|
--health-cmd "redis-cli ping"
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.11"
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
cd backend
|
|
pip install --upgrade pip
|
|
pip install -e ".[dev]"
|
|
|
|
- name: Run linter
|
|
run: |
|
|
cd backend
|
|
ruff check .
|
|
|
|
- name: Run type checker (optional)
|
|
continue-on-error: true
|
|
run: |
|
|
cd backend
|
|
pip install mypy
|
|
mypy app --ignore-missing-imports
|
|
|
|
- name: Run migrations
|
|
env:
|
|
DATABASE_URL: postgresql+asyncpg://wrongbook:wrongbook@localhost:5433/wrongbook_test
|
|
run: |
|
|
cd backend
|
|
alembic upgrade head
|
|
|
|
- name: Run tests with coverage
|
|
env:
|
|
TEST_DATABASE_URL: postgresql+asyncpg://wrongbook:wrongbook@localhost:5433/wrongbook_test
|
|
REDIS_URL: redis://localhost:6380/0
|
|
ANTHROPIC_AUTH_TOKEN: sk-ant-test-placeholder
|
|
ANTHROPIC_BASE_URL: https://api.anthropic.com
|
|
ANTHROPIC_MODEL: claude-sonnet-4-6
|
|
TESTING: "true"
|
|
JWT_SECRET: test-secret-key
|
|
run: |
|
|
cd backend
|
|
pytest --cov=app --cov-report=xml --cov-report=term --cov-fail-under=50 -v
|
|
|
|
build-and-push:
|
|
name: Build and Push Image
|
|
runs-on: ubuntu-latest
|
|
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop'
|
|
timeout-minutes: 30
|
|
outputs:
|
|
image_alias: ${{ steps.meta.outputs.image_alias }}
|
|
image_name: ${{ steps.meta.outputs.image_name }}
|
|
image_ref: ${{ steps.meta.outputs.image_ref }}
|
|
image_tag: ${{ steps.meta.outputs.image_tag }}
|
|
image_digest: ${{ steps.build.outputs.digest }}
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Resolve metadata
|
|
id: meta
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
HARBOR_HOST="${HARBOR_HOST_OVERRIDE:-harbor.omniverseai.net}"
|
|
HARBOR_PROJECT="${HARBOR_PROJECT_OVERRIDE:-ljwx}"
|
|
IMAGE_ALIAS="${IMAGE_NAME_OVERRIDE:-ljwx-shiti}"
|
|
IMAGE_FULL_NAME="${HARBOR_HOST}/${HARBOR_PROJECT}/${IMAGE_ALIAS}"
|
|
IMAGE_TAG="${GITHUB_SHA:0:7}"
|
|
|
|
{
|
|
echo "harbor_host=${HARBOR_HOST}"
|
|
echo "image_alias=${IMAGE_ALIAS}"
|
|
echo "image_name=${IMAGE_FULL_NAME}"
|
|
echo "image_ref=${IMAGE_FULL_NAME}:${IMAGE_TAG}"
|
|
echo "image_tag=${IMAGE_TAG}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Login to Harbor
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ steps.meta.outputs.harbor_host }}
|
|
username: ${{ secrets.HARBOR_USERNAME }}
|
|
password: ${{ secrets.HARBOR_PASSWORD }}
|
|
|
|
- name: Build and push
|
|
uses: docker/build-push-action@v6
|
|
id: build
|
|
with:
|
|
context: .
|
|
file: backend/Dockerfile
|
|
push: true
|
|
tags: |
|
|
${{ steps.meta.outputs.image_ref }}
|
|
${{ steps.meta.outputs.image_name }}:latest
|
|
platforms: linux/amd64
|
|
|
|
update-deploy:
|
|
name: Update Deploy Repo
|
|
runs-on: ubuntu-latest
|
|
needs: build-and-push
|
|
if: needs.build-and-push.result == 'success'
|
|
timeout-minutes: 10
|
|
outputs:
|
|
pr_url: ${{ steps.create_pr.outputs.pr_url }}
|
|
|
|
steps:
|
|
- name: Install PyYAML
|
|
shell: bash
|
|
run: python3 -m pip install --quiet pyyaml
|
|
|
|
- name: Resolve deploy config
|
|
id: cfg
|
|
env:
|
|
DEPLOY_REPO_OVERRIDE: ${{ secrets.DEPLOY_REPO }}
|
|
DEPLOY_OVERLAY_FILE_OVERRIDE: ${{ secrets.DEPLOY_OVERLAY_FILE }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
DEPLOY_REPO="${DEPLOY_REPO_OVERRIDE:-gao/ljwx-deploy}"
|
|
OVERLAY_FILE="${DEPLOY_OVERLAY_FILE_OVERRIDE:-apps/ljwx-shiti/overlays/prod/kustomization.yaml}"
|
|
{
|
|
echo "deploy_repo=${DEPLOY_REPO}"
|
|
echo "overlay_file=${OVERLAY_FILE}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Clone deploy repo
|
|
env:
|
|
DEPLOY_REPO: ${{ steps.cfg.outputs.deploy_repo }}
|
|
DEPLOY_REPO_TOKEN: ${{ secrets.DEPLOY_REPO_TOKEN }}
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
GITEA_HOST: ${{ secrets.GITEA_HOST }}
|
|
GITEA_BASE_URL: ${{ secrets.GITEA_BASE_URL }}
|
|
GITEA_USERNAME: ${{ secrets.GITEA_USERNAME }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
TOKEN="${DEPLOY_REPO_TOKEN:-${GITEA_TOKEN:-}}"
|
|
if [ -z "${TOKEN:-}" ]; then
|
|
echo "DEPLOY_REPO_TOKEN or GITEA_TOKEN is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
BASE_URL="${GITEA_BASE_URL:-}"
|
|
if [ -z "${BASE_URL}" ]; then
|
|
if [ -n "${GITEA_HOST:-}" ]; then
|
|
case "${GITEA_HOST}" in
|
|
http://*|https://*) BASE_URL="${GITEA_HOST}" ;;
|
|
*) BASE_URL="http://${GITEA_HOST}" ;;
|
|
esac
|
|
else
|
|
BASE_URL="http://192.168.1.83:33000"
|
|
fi
|
|
fi
|
|
BASE_URL="${BASE_URL%/}"
|
|
|
|
AUTH_USER="${GITEA_USERNAME:-${DEPLOY_REPO%%/*}}"
|
|
case "${BASE_URL}" in
|
|
https://*) AUTH_BASE="https://${AUTH_USER}:${TOKEN}@${BASE_URL#https://}" ;;
|
|
http://*) AUTH_BASE="http://${AUTH_USER}:${TOKEN}@${BASE_URL#http://}" ;;
|
|
*) AUTH_BASE="http://${AUTH_USER}:${TOKEN}@${BASE_URL}" ;;
|
|
esac
|
|
|
|
git clone "${AUTH_BASE}/${DEPLOY_REPO}.git" deploy-repo
|
|
|
|
- name: Update overlay kustomization
|
|
env:
|
|
OVERLAY_FILE: ${{ steps.cfg.outputs.overlay_file }}
|
|
IMAGE_FULL_NAME: ${{ needs.build-and-push.outputs.image_name }}
|
|
IMAGE_ALIAS: ${{ needs.build-and-push.outputs.image_alias }}
|
|
IMAGE_TAG: ${{ needs.build-and-push.outputs.image_tag }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
python3 - <<'PY'
|
|
import os
|
|
from pathlib import Path
|
|
import yaml
|
|
|
|
overlay = Path("deploy-repo") / os.environ["OVERLAY_FILE"]
|
|
image_full_name = os.environ["IMAGE_FULL_NAME"]
|
|
image_alias = os.environ.get("IMAGE_ALIAS") or image_full_name.rsplit("/", 1)[-1]
|
|
image_tag = os.environ["IMAGE_TAG"]
|
|
|
|
if not overlay.exists():
|
|
overlay.parent.mkdir(parents=True, exist_ok=True)
|
|
data = {
|
|
"apiVersion": "kustomize.config.k8s.io/v1beta1",
|
|
"kind": "Kustomization",
|
|
"namespace": "ljwx-shiti",
|
|
"resources": ["../../base"],
|
|
"images": [],
|
|
}
|
|
else:
|
|
data = yaml.safe_load(overlay.read_text()) or {}
|
|
|
|
if not isinstance(data, dict):
|
|
data = {}
|
|
images = data.setdefault("images", [])
|
|
if not isinstance(images, list):
|
|
images = []
|
|
data["images"] = images
|
|
|
|
found = False
|
|
for img in images:
|
|
if not isinstance(img, dict):
|
|
continue
|
|
name = str(img.get("name", ""))
|
|
new_name = str(img.get("newName", ""))
|
|
if image_full_name in (name, new_name) or image_alias in (name, new_name):
|
|
img["name"] = image_alias
|
|
img["newName"] = image_full_name
|
|
img["newTag"] = image_tag
|
|
found = True
|
|
break
|
|
|
|
if not found:
|
|
images.append(
|
|
{
|
|
"name": image_alias,
|
|
"newName": image_full_name,
|
|
"newTag": image_tag,
|
|
}
|
|
)
|
|
|
|
overlay.write_text(yaml.safe_dump(data, sort_keys=False))
|
|
print(f"Updated {overlay} -> {image_full_name}:{image_tag}")
|
|
PY
|
|
|
|
- name: Create deploy PR
|
|
id: create_pr
|
|
working-directory: deploy-repo
|
|
env:
|
|
DEPLOY_REPO: ${{ steps.cfg.outputs.deploy_repo }}
|
|
OVERLAY_FILE: ${{ steps.cfg.outputs.overlay_file }}
|
|
DEPLOY_REPO_TOKEN: ${{ secrets.DEPLOY_REPO_TOKEN }}
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
GITEA_HOST: ${{ secrets.GITEA_HOST }}
|
|
GITEA_BASE_URL: ${{ secrets.GITEA_BASE_URL }}
|
|
GIT_SHA: ${{ github.sha }}
|
|
RUN_ID: ${{ github.run_id }}
|
|
SOURCE_REPO: ${{ github.repository }}
|
|
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
|
IMAGE_REF: ${{ needs.build-and-push.outputs.image_ref }}
|
|
IMAGE_DIGEST: ${{ needs.build-and-push.outputs.image_digest }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
TOKEN="${DEPLOY_REPO_TOKEN:-${GITEA_TOKEN:-}}"
|
|
if [ -z "${TOKEN:-}" ]; then
|
|
echo "DEPLOY_REPO_TOKEN or GITEA_TOKEN is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
BASE_URL="${GITEA_BASE_URL:-}"
|
|
if [ -z "${BASE_URL}" ]; then
|
|
if [ -n "${GITEA_HOST:-}" ]; then
|
|
case "${GITEA_HOST}" in
|
|
http://*|https://*) BASE_URL="${GITEA_HOST}" ;;
|
|
*) BASE_URL="http://${GITEA_HOST}" ;;
|
|
esac
|
|
else
|
|
BASE_URL="http://192.168.1.83:33000"
|
|
fi
|
|
fi
|
|
BASE_URL="${BASE_URL%/}"
|
|
|
|
SHA_SHORT="${GIT_SHA:0:7}"
|
|
RUN_SUFFIX="${RUN_ID:-$(date +%s)}"
|
|
BRANCH="bot/release-ljwx-shiti-${RUN_SUFFIX}"
|
|
|
|
git config user.name "gitea-actions[bot]"
|
|
git config user.email "gitea-actions[bot]@users.noreply.gitea.io"
|
|
git checkout -b "${BRANCH}"
|
|
git add "${OVERLAY_FILE}"
|
|
|
|
if git diff --staged --quiet; then
|
|
echo "No changes to commit"
|
|
echo "pr_url=" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
git commit -m "release(ljwx-shiti): ${SHA_SHORT}"
|
|
git push origin "${BRANCH}"
|
|
|
|
BASE_BRANCH="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')"
|
|
BASE_BRANCH="${BASE_BRANCH:-main}"
|
|
|
|
PR_TITLE="release(ljwx-shiti): ${SHA_SHORT}"
|
|
PR_BODY="$(cat <<EOF
|
|
Automated release PR from ${SOURCE_REPO}@${GIT_SHA}
|
|
|
|
Image: ${IMAGE_REF}
|
|
digest: ${IMAGE_DIGEST}
|
|
|
|
Changes: ${COMMIT_MESSAGE:-N/A}
|
|
EOF
|
|
)"
|
|
|
|
export BASE_URL DEPLOY_REPO TOKEN BRANCH BASE_BRANCH PR_TITLE PR_BODY
|
|
PR_URL="$(python3 - <<'PY'
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
api = f"{os.environ['BASE_URL']}/api/v1/repos/{os.environ['DEPLOY_REPO']}/pulls"
|
|
payload = {
|
|
"title": os.environ["PR_TITLE"],
|
|
"head": os.environ["BRANCH"],
|
|
"base": os.environ["BASE_BRANCH"],
|
|
"body": os.environ["PR_BODY"],
|
|
}
|
|
request = urllib.request.Request(
|
|
api,
|
|
data=json.dumps(payload).encode("utf-8"),
|
|
headers={
|
|
"Authorization": f"token {os.environ['TOKEN']}",
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
},
|
|
method="POST",
|
|
)
|
|
|
|
try:
|
|
with urllib.request.urlopen(request) as response:
|
|
data = json.loads(response.read().decode("utf-8"))
|
|
except urllib.error.HTTPError as error:
|
|
detail = error.read().decode("utf-8", errors="replace")
|
|
print(detail, file=sys.stderr)
|
|
raise
|
|
|
|
print(data.get("html_url", ""))
|
|
PY
|
|
)"
|
|
|
|
if [ -z "${PR_URL}" ]; then
|
|
echo "Failed to parse PR URL from Gitea API response" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT"
|
|
echo "Created PR: ${PR_URL}"
|
|
|
|
summary:
|
|
name: Fast Deploy Summary
|
|
runs-on: ubuntu-latest
|
|
needs: [build-and-push, update-deploy]
|
|
if: always()
|
|
steps:
|
|
- name: Generate summary
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
SUMMARY_FILE="${GITHUB_STEP_SUMMARY:-/tmp/gitea-step-summary.md}"
|
|
|
|
{
|
|
echo "## Gitea Fast Deploy Summary"
|
|
echo
|
|
echo "- Build: ${{ needs.build-and-push.result }}"
|
|
echo "- Deploy: ${{ needs.update-deploy.result }}"
|
|
if [ "${{ needs.build-and-push.result }}" = "success" ]; then
|
|
echo "- Image: ${{ needs.build-and-push.outputs.image_ref }}"
|
|
fi
|
|
if [ -n "${{ needs.update-deploy.outputs.pr_url }}" ]; then
|
|
echo "- Deploy PR: ${{ needs.update-deploy.outputs.pr_url }}"
|
|
fi
|
|
} >> "${SUMMARY_FILE}"
|
|
|
|
cat "${SUMMARY_FILE}"
|