ci(gitea): add fast deploy workflow with harbor + deploy PR

This commit is contained in:
2026-02-28 23:22:26 +08:00
parent f58fddb856
commit 50ad9fded9
+463
View File
@@ -0,0 +1,463 @@
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 }}
USE_CN_MIRROR_OVERRIDE: ${{ secrets.USE_CN_MIRROR }}
jobs:
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: 45
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
env:
GITEA_BASE_URL: ${{ secrets.GITEA_BASE_URL }}
GITEA_HOST: ${{ secrets.GITEA_HOST }}
shell: bash
run: |
set -euo pipefail
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%/}"
git clone "${BASE_URL}/${GITHUB_REPOSITORY}.git" repo
cd repo
git fetch --depth 1 origin "${GITHUB_SHA}"
git checkout --detach "${GITHUB_SHA}"
- 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-chat}"
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: Ensure Docker CLI
shell: bash
run: |
set -euo pipefail
if ! command -v docker >/dev/null 2>&1; then
if command -v apk >/dev/null 2>&1; then
apk add --no-cache docker-cli
elif command -v apt-get >/dev/null 2>&1; then
apt-get update
apt-get install -y docker.io
else
echo "No supported package manager found to install docker CLI" >&2
exit 1
fi
fi
docker version
- name: Login to Harbor
shell: bash
run: |
set -euo pipefail
HARBOR_HOST="${{ steps.meta.outputs.harbor_host }}"
PRIMARY_USER="${{ secrets.HARBOR_USERNAME }}"
PRIMARY_PASS="${{ secrets.HARBOR_PASSWORD }}"
FALLBACK_USER="admin"
FALLBACK_PASS="admin123"
if [ -n "${PRIMARY_USER}" ] && [ -n "${PRIMARY_PASS}" ]; then
if echo "${PRIMARY_PASS}" | docker login "${HARBOR_HOST}" -u "${PRIMARY_USER}" --password-stdin; then
exit 0
fi
echo "Primary Harbor credentials failed, trying local fallback user..." >&2
fi
echo "${FALLBACK_PASS}" | docker login "${HARBOR_HOST}" -u "${FALLBACK_USER}" --password-stdin
- name: Build and push
id: build
shell: bash
run: |
set -euo pipefail
IMAGE_REF="${{ steps.meta.outputs.image_ref }}"
IMAGE_NAME="${{ steps.meta.outputs.image_name }}"
USE_CN_MIRROR="${USE_CN_MIRROR_OVERRIDE:-true}"
docker build \
-f repo/Dockerfile \
--build-arg USE_CN_MIRROR="${USE_CN_MIRROR}" \
-t "${IMAGE_REF}" \
-t "${IMAGE_NAME}:latest" \
repo
docker push "${IMAGE_REF}"
docker push "${IMAGE_NAME}:latest"
DIGEST="$(docker inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${IMAGE_REF}" \
| awk -F'@' 'NF==2 {print $2; exit}')"
if [ -z "${DIGEST}" ]; then
echo "Failed to resolve digest for ${IMAGE_REF}" >&2
exit 1
fi
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
update-deploy:
name: Update Deploy Repo
runs-on: ubuntu-latest
needs: build-and-push
if: needs.build-and-push.result == 'success'
timeout-minutes: 12
outputs:
pr_url: ${{ steps.create_pr.outputs.pr_url }}
steps:
- name: Ensure Python runtime
shell: bash
run: |
set -euo pipefail
if command -v python3 >/dev/null 2>&1; then
:
elif command -v python >/dev/null 2>&1; then
ln -sf "$(command -v python)" /usr/local/bin/python3
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache python3 py3-pip
elif command -v apt-get >/dev/null 2>&1; then
apt-get update
apt-get install -y python3 python3-pip
else
echo "No supported package manager found to install python3" >&2
exit 1
fi
if ! python3 -m pip --version >/dev/null 2>&1; then
if command -v apk >/dev/null 2>&1; then
apk add --no-cache py3-pip
elif command -v apt-get >/dev/null 2>&1; then
apt-get update
apt-get install -y python3-pip
else
echo "python3 exists but pip is unavailable" >&2
exit 1
fi
fi
python3 --version
python3 -m pip --version
- name: Install PyYAML
shell: bash
run: |
set -euo pipefail
PIP_INDEX_URL="${{ secrets.PIP_INDEX_URL }}"
PIP_EXTRA_INDEX_URL="${{ secrets.PIP_EXTRA_INDEX_URL }}"
PIP_TRUSTED_HOST="${{ secrets.PIP_TRUSTED_HOST }}"
if [ -z "${PIP_INDEX_URL}" ]; then
PIP_INDEX_URL="https://pypi.tuna.tsinghua.edu.cn/simple"
fi
if [ -z "${PIP_EXTRA_INDEX_URL}" ]; then
PIP_EXTRA_INDEX_URL="https://pypi.org/simple"
fi
if [ -z "${PIP_TRUSTED_HOST}" ]; then
PIP_TRUSTED_HOST="pypi.tuna.tsinghua.edu.cn"
fi
PIP_INDEX_URL="${PIP_INDEX_URL}" \
PIP_EXTRA_INDEX_URL="${PIP_EXTRA_INDEX_URL}" \
PIP_TRUSTED_HOST="${PIP_TRUSTED_HOST}" \
python3 -m pip install --quiet --timeout 120 --retries 10 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-chat/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": "chat",
"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-chat-${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-chat): ${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-chat): ${SHA_SHORT}"
PR_BODY="$(cat <<EOF2
Automated release PR from ${SOURCE_REPO}@${GIT_SHA}
Image: ${IMAGE_REF}
digest: ${IMAGE_DIGEST}
Changes: ${COMMIT_MESSAGE:-N/A}
EOF2
)"
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}"