270 lines
9.9 KiB
YAML
270 lines
9.9 KiB
YAML
name: Build Any Repository Docker Image
|
||
|
||
on:
|
||
workflow_dispatch:
|
||
inputs:
|
||
repo_url:
|
||
description: '要构建的仓库地址(支持相对路径如:xc-workflows/engine-demo,或完整 URL 如:https://dev.modelhub.org.cn/xc-workflows/engine-demo)'
|
||
required: true
|
||
type: string
|
||
ref:
|
||
description: '分支/tag/commit'
|
||
required: true
|
||
default: 'main'
|
||
type: string
|
||
dry_run:
|
||
description: '仅构建不推送'
|
||
required: false
|
||
default: 'false'
|
||
type: choice
|
||
options:
|
||
- 'true'
|
||
- 'false'
|
||
job_id:
|
||
description: '任务ID'
|
||
required: true
|
||
type: string
|
||
submission_id:
|
||
description: '提交ID'
|
||
required: true
|
||
type: string
|
||
callback_url:
|
||
description: '回调地址'
|
||
required: false
|
||
default: ''
|
||
type: string
|
||
callback_token:
|
||
description: '回调Token'
|
||
required: false
|
||
default: ''
|
||
type: string
|
||
startup_params:
|
||
description: '启动参数(JSON格式)'
|
||
required: false
|
||
default: '{}'
|
||
type: string
|
||
|
||
jobs:
|
||
build:
|
||
runs-on: amd64-ubuntu-24.04
|
||
|
||
steps:
|
||
- name: Debug URL info
|
||
run: |
|
||
echo "Server URL: ${{ gitea.server_url }}"
|
||
echo "Repo URL input: ${{ github.event.inputs.repo_url }}"
|
||
echo "Ref: ${{ github.event.inputs.ref }}"
|
||
echo "Dry run: ${{ github.event.inputs.dry_run }}"
|
||
echo "Job ID: ${{ github.event.inputs.job_id }}"
|
||
echo "Submission ID: ${{ github.event.inputs.submission_id }}"
|
||
echo "Callback URL: ${{ github.event.inputs.callback_url }}"
|
||
echo "Startup Params: ${{ github.event.inputs.startup_params }}"
|
||
|
||
- name: Clone target repository
|
||
run: |
|
||
REPO_URL="${{ github.event.inputs.repo_url }}"
|
||
|
||
# 判断是否完整 URL
|
||
if [[ "$REPO_URL" == http://* || "$REPO_URL" == https://* ]]; then
|
||
CLONE_URL="$REPO_URL"
|
||
# 如果没有 .git 后缀,加上
|
||
[[ "$CLONE_URL" != *.git ]] && CLONE_URL="${CLONE_URL}.git"
|
||
# 提取路径部分用于生成镜像名
|
||
REPO_PATH="$(echo "$REPO_URL" | sed 's|https\?://[^/]*/||' | sed 's|\.git$||')"
|
||
else
|
||
# 拼接完整的 clone URL(gitea.server_url 已包含协议)
|
||
CLONE_URL="${{ gitea.server_url }}/$REPO_URL.git"
|
||
REPO_PATH="$REPO_URL"
|
||
fi
|
||
|
||
echo "正在克隆: $CLONE_URL"
|
||
|
||
git clone "$CLONE_URL" target_repo
|
||
cd target_repo
|
||
git checkout "${{ github.event.inputs.ref }}"
|
||
|
||
# 打 tag
|
||
TAG_NAME="build-submission${{ github.event.inputs.submission_id }}-job${{ github.event.inputs.job_id }}"
|
||
git tag "$TAG_NAME"
|
||
echo "已打 tag: $TAG_NAME"
|
||
|
||
# 保存仓库信息供后续步骤使用
|
||
echo "TARGET_REPO_PATH=$(pwd)" >> "$GITEA_ENV"
|
||
echo "TARGET_REPO_NAME=$(basename "$REPO_PATH")" >> "$GITEA_ENV"
|
||
echo "REPO_PATH=$REPO_PATH" >> "$GITEA_ENV"
|
||
echo "TAG_NAME=$TAG_NAME" >> "$GITEA_ENV"
|
||
echo "克隆完成,当前目录: $(pwd)"
|
||
|
||
- name: Set image metadata
|
||
run: |
|
||
cd target_repo
|
||
|
||
# 生成镜像名称(将仓库路径中的 / 替换为 -,并统一归到 judge-flows 命名空间下)
|
||
IMAGE_NAME="judge-flows/$(echo "$REPO_PATH" | tr '[:upper:]' '[:lower:]' | tr '/' '-')"
|
||
SAFE_TAG="${TAG_NAME}"
|
||
IMAGE="${DOCKER_REGISTRY}/${DOCKER_USERNAME}/${IMAGE_NAME}:${SAFE_TAG}"
|
||
|
||
echo "IMAGE_NAME=${IMAGE_NAME}" >> "$GITEA_ENV"
|
||
echo "IMAGE=${IMAGE}" >> "$GITEA_ENV"
|
||
echo "SAFE_TAG=${SAFE_TAG}" >> "$GITEA_ENV"
|
||
echo "镜像名称: ${IMAGE}"
|
||
|
||
- name: Check Dockerfile exists
|
||
id: check-dockerfile
|
||
run: |
|
||
cd target_repo
|
||
if [ ! -f "Dockerfile" ]; then
|
||
echo "错误: 仓库根目录下未找到 Dockerfile"
|
||
exit 1
|
||
fi
|
||
echo "Dockerfile 存在"
|
||
|
||
- name: Login to Docker Registry
|
||
id: login-registry
|
||
if: github.event.inputs.dry_run != 'true'
|
||
run: |
|
||
echo "$DOCKER_PASSWORD" | docker login "$DOCKER_REGISTRY" \
|
||
-u "$DOCKER_USERNAME" \
|
||
--password-stdin
|
||
echo "Docker 登录成功"
|
||
|
||
- name: Build Docker Image
|
||
id: build-image
|
||
run: |
|
||
cd target_repo
|
||
echo "开始构建镜像: ${IMAGE}"
|
||
set -o pipefail
|
||
docker build -t "${IMAGE}" . 2>&1 | tee build.log
|
||
echo "镜像构建完成"
|
||
|
||
- name: List built image
|
||
run: |
|
||
docker images | grep "${IMAGE_NAME}" || echo "镜像列表查看完成"
|
||
|
||
- name: Push Docker Image
|
||
id: push-image
|
||
if: github.event.inputs.dry_run != 'true'
|
||
run: |
|
||
set -o pipefail
|
||
{
|
||
for attempt in 1 2 3; do
|
||
echo "Starting docker push attempt ${attempt}/3 for ${IMAGE}"
|
||
|
||
if docker push "${IMAGE}"; then
|
||
echo "docker push completed successfully"
|
||
exit 0
|
||
fi
|
||
|
||
echo "docker push failed on attempt ${attempt}/3"
|
||
if [ $attempt -lt 3 ]; then
|
||
echo "等待 30 秒后重试..."
|
||
sleep 30
|
||
fi
|
||
done
|
||
|
||
echo "docker push failed after 3 attempts"
|
||
exit 1
|
||
} 2>&1 | tee push.log
|
||
|
||
- name: Dry Run Summary
|
||
if: github.event.inputs.dry_run == 'true'
|
||
run: |
|
||
echo "=========================================="
|
||
echo "✅ 仅构建模式完成(未推送)"
|
||
echo "=========================================="
|
||
echo "源仓库: ${{ github.event.inputs.repo_url }}"
|
||
echo "分支: ${{ github.event.inputs.ref }}"
|
||
echo "镜像名称: ${IMAGE}"
|
||
echo "镜像标签: ${SAFE_TAG}"
|
||
echo "=========================================="
|
||
echo "如需完整发布(推送镜像),请使用 dry_run=false 重新触发"
|
||
|
||
- name: Full Build Summary
|
||
if: github.event.inputs.dry_run != 'true'
|
||
run: |
|
||
echo "=========================================="
|
||
echo "✅ 完整构建完成(已推送)"
|
||
echo "=========================================="
|
||
echo "源仓库: ${{ github.event.inputs.repo_url }}"
|
||
echo "分支: ${{ github.event.inputs.ref }}"
|
||
echo "镜像名称: ${IMAGE}"
|
||
echo "镜像标签: ${SAFE_TAG}"
|
||
echo "=========================================="
|
||
|
||
- name: Callback notification
|
||
if: always() && github.event.inputs.callback_url != ''
|
||
run: |
|
||
echo "正在发送回调通知..."
|
||
|
||
# 构造回调请求体
|
||
CALLBACK_URL="${{ github.event.inputs.callback_url }}"
|
||
CALLBACK_TOKEN="${{ github.event.inputs.callback_token }}"
|
||
JOB_ID="${{ github.event.inputs.job_id }}"
|
||
STARTUP_PARAMS='${{ github.event.inputs.startup_params }}'
|
||
|
||
# 判断状态与错误信息
|
||
STATUS="SUCCESS"
|
||
ERR_MESSAGE=""
|
||
|
||
if [ "${{ steps.check-dockerfile.outcome }}" == "failure" ]; then
|
||
STATUS="FAILED"
|
||
ERR_MESSAGE="仓库根目录下未找到 Dockerfile"
|
||
elif [ "${{ steps.login-registry.outcome }}" == "failure" ]; then
|
||
STATUS="FAILED"
|
||
ERR_MESSAGE="Docker 镜像仓库登录失败"
|
||
elif [ "${{ steps.build-image.outcome }}" == "failure" ]; then
|
||
STATUS="FAILED"
|
||
ERR_MESSAGE=$(tail -n 20 build.log 2>/dev/null || echo "Docker 镜像构建失败")
|
||
elif [ "${{ steps.push-image.outcome }}" == "failure" ]; then
|
||
STATUS="FAILED"
|
||
ERR_MESSAGE=$(tail -n 20 push.log 2>/dev/null || echo "Docker 镜像推送失败")
|
||
fi
|
||
|
||
# dry_run 为 true 时,回调使用固定镜像地址
|
||
if [ "${{ github.event.inputs.dry_run }}" == "true" ]; then
|
||
IMAGE_URL="harbor-contest.4pd.io/luopingyi/enginex-iluvatar-bi150/vllm:0.8.3"
|
||
else
|
||
IMAGE_URL="$IMAGE"
|
||
fi
|
||
|
||
# 使用 jq 构造 JSON 请求体
|
||
if [ -n "$ERR_MESSAGE" ]; then
|
||
BODY=$(jq -n \
|
||
--arg jobId "$JOB_ID" \
|
||
--arg imageUrl "$IMAGE_URL" \
|
||
--arg status "$STATUS" \
|
||
--arg errMessage "$ERR_MESSAGE" \
|
||
--argjson startupParams "$STARTUP_PARAMS" \
|
||
'{jobId: $jobId, imageUrl: $imageUrl, status: $status, errMessage: $errMessage, startupParams: $startupParams}')
|
||
else
|
||
BODY=$(jq -n \
|
||
--arg jobId "$JOB_ID" \
|
||
--arg imageUrl "$IMAGE_URL" \
|
||
--arg status "$STATUS" \
|
||
--argjson startupParams "$STARTUP_PARAMS" \
|
||
'{jobId: $jobId, imageUrl: $imageUrl, status: $status, startupParams: $startupParams}')
|
||
fi
|
||
|
||
echo "回调请求体: $BODY"
|
||
|
||
# 发送 POST 请求
|
||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
||
-X POST \
|
||
-H "Content-Type: application/json" \
|
||
-H "X-Callback-Token: $CALLBACK_TOKEN" \
|
||
-d "$BODY" \
|
||
"$CALLBACK_URL" || true)
|
||
|
||
# 去除空白并校验 HTTP_CODE 是否为数字
|
||
HTTP_CODE=$(echo "$HTTP_CODE" | tr -d '[:space:]')
|
||
if ! [[ "$HTTP_CODE" =~ ^[0-9]+$ ]]; then
|
||
HTTP_CODE="000"
|
||
fi
|
||
|
||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||
echo "✅ 回调通知发送成功,HTTP 状态码: $HTTP_CODE"
|
||
else
|
||
echo "❌ 回调通知发送失败,HTTP 状态码: $HTTP_CODE"
|
||
exit 1
|
||
fi
|