name: CI on: pull_request: branches: - master types: - closed jobs: build-deploy: runs-on: act_runner_java if: ${{ github.event.pull_request.merged == true }} outputs: deployment_status: ${{ steps.set_status.outputs.status }} env: JAVA_HOME: /usr/lib/jvm/java-21-openjdk steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up environment run: | echo "PR #${{ github.event.number }} merged into master" echo "Source branch: ${{ github.event.pull_request.head.ref }}" echo "Target branch: ${{ github.event.pull_request.base.ref }}" - name: Run tests run: | echo "Running test suite..." - name: Setup Maven settings run: | if [ -z "${{ vars.TIMI_NEXUS_USERNAME }}" ] || [ -z "${{ vars.TIMI_NEXUS_PASSWORD }}" ]; then echo "Missing vars.TIMI_NEXUS_USERNAME or vars.TIMI_NEXUS_PASSWORD" exit 1 fi mkdir -p ~/.m2 cat > ~/.m2/settings.xml < timi_nexus ${{ vars.TIMI_NEXUS_USERNAME }} ${{ vars.TIMI_NEXUS_PASSWORD }} EOF - name: Build project run: | mvn -B -DskipTests clean package -P prod-linux - name: Deploy service if: success() env: HOST: host.docker.internal APP_PATH: ${{ vars.APP_PATH }} DOCKER_CONTAINER_NAME: ${{ vars.DOCKER_CONTAINER_NAME }} SSHPASS: ${{ secrets.TIMI_SERVER_SSH_PWD }} MAX_RETRIES: 3 RETRY_DELAY: 10 run: | if [ -z "$HOST" ] || [ -z "$APP_PATH" ] || [ -z "DOCKER_CONTAINER_NAME" ] || [ -z "$SSHPASS" ]; then echo "Missing production environment variables" echo "Required: APP_PATH, DOCKER_CONTAINER_NAME, TIMI_SERVER_SSH_PWD" exit 1 fi # 重试函数 retry_command() { local cmd="$1" local desc="$2" local attempt=1 while [ $attempt -le $MAX_RETRIES ]; do echo "[$desc] Attempt $attempt/$MAX_RETRIES..." if eval "$cmd"; then echo "✓ $desc succeeded" return 0 fi echo "✗ $desc failed (attempt $attempt/$MAX_RETRIES)" if [ $attempt -lt $MAX_RETRIES ]; then echo "Retrying in ${RETRY_DELAY}s..." sleep $RETRY_DELAY fi attempt=$((attempt + 1)) done echo "✗ $desc failed after $MAX_RETRIES attempts" return 1 } # SSH 配置(使用密码认证) SSH_PORT="22" SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=30 -o ServerAliveInterval=10 -o ServerAliveCountMax=3 -p $SSH_PORT" SCP_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=30 -o ServerAliveInterval=10 -o ServerAliveCountMax=3 -P $SSH_PORT" # 获取构建产物信息 version=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version) artifact_id=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.artifactId) jar_file="target/${artifact_id}-${version}.jar" if [ ! -f "$jar_file" ]; then echo "Build artifact not found: $jar_file" exit 1 fi # 目标文件名(去掉版本号) target_jar="${artifact_id}.jar" echo "Deploying $jar_file to $HOST:$APP_PATH/$target_jar" # 上传文件(带重试) if ! retry_command "sshpass -e scp $SCP_OPTS \"$jar_file\" \"root@$HOST:$APP_PATH/$target_jar\"" "SCP upload"; then exit 1 fi # 重启 Docker 服务(带重试) echo "Restarting Docker service: $DOCKER_SERVICE_NAME" if ! retry_command "sshpass -e ssh $SSH_OPTS \"root@$HOST\" \"docker restart $DOCKER_SERVICE_NAME\"" "Docker restart"; then exit 1 fi echo "Deployment completed successfully" - name: Create release if: ${{ success() && startsWith(github.event.pull_request.title, 'v') }} env: GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }} GITEA_SERVER_URL: ${{ github.server_url }} GITEA_INTERNAL_URL: ${{ vars.TIMI_GITEA_INTERNAL_URL }} GITEA_REPOSITORY: ${{ github.repository }} RELEASE_TAG: ${{ github.event.pull_request.title }} RELEASE_TARGET: ${{ github.sha }} MAX_RETRIES: 3 RETRY_DELAY: 10 run: | if [ -z "$GITEA_TOKEN" ]; then echo "Missing secrets.RUNNER_TOKEN" exit 1 fi # Use internal URL if available, fallback to public URL if [ -n "$GITEA_INTERNAL_URL" ]; then api_base_url="$GITEA_INTERNAL_URL" echo "Using internal Gitea URL: $api_base_url" else api_base_url="$GITEA_SERVER_URL" echo "Using public Gitea URL: $api_base_url" fi # 获取构建产物信息 version=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version) artifact_id=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.artifactId) jar_file="target/${artifact_id}-${version}.jar" if [ ! -f "$jar_file" ]; then echo "Build artifact not found: $jar_file" exit 1 fi file_size=$(stat -c%s "$jar_file" 2>/dev/null || stat -f%z "$jar_file" 2>/dev/null || echo "unknown") echo "Found fat jar: $jar_file (size: $file_size bytes)" api_url="$api_base_url/api/v1/repos/$GITEA_REPOSITORY/releases" payload=$(cat < "$release_response_file" http_code=$(curl -sS -w "%{http_code}" -o "$release_response_file" -X POST "$api_url" \ -H "Authorization: token $GITEA_TOKEN" \ -H "Content-Type: application/json" \ --connect-timeout 30 \ --max-time 60 \ -d "$payload" 2>/dev/null) || http_code="000" response=$(cat "$release_response_file" 2>/dev/null || echo "{}") echo "HTTP Status: $http_code" if [ "$http_code" = "201" ]; then # 提取第一个 id 字段的值,确保去除换行符 if command -v jq >/dev/null 2>&1; then release_id=$(echo "$response" | jq -r '.id' 2>/dev/null) else release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 | tr -d '\n\r') fi echo "✓ Release created: id=$release_id" elif [ "$http_code" = "409" ]; then # HTTP 409 Conflict: Release 已存在,获取现有的 release_id echo "Release already exists (HTTP 409), fetching existing release..." existing=$(curl -sS "$api_url" -H "Authorization: token $GITEA_TOKEN" --connect-timeout 30 2>/dev/null || echo "[]") # 使用 jq 解析 JSON,如果没有 jq 则用 grep if command -v jq >/dev/null 2>&1; then release_id=$(echo "$existing" | jq -r ".[] | select(.tag_name==\"$RELEASE_TAG\") | .id" 2>/dev/null | head -1) else release_id=$(echo "$existing" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 | tr -d '\n\r') fi if [ -n "$release_id" ]; then echo "✓ Found existing release: id=$release_id" else echo "✗ Could not find existing release id" fi else echo "✗ Failed (HTTP $http_code)" if [ $attempt -lt $MAX_RETRIES ]; then echo "Retrying in ${RETRY_DELAY}s..." sleep $RETRY_DELAY fi fi attempt=$((attempt + 1)) done if [ -z "$release_id" ]; then echo "✗ Failed to create/find release after $MAX_RETRIES attempts" exit 1 fi # 上传 fat jar(带重试) asset_name=$(basename "$jar_file") echo "Uploading asset: $asset_name (size: $file_size bytes)" upload_url="$api_url/$release_id/assets?name=$asset_name" echo "Upload URL: $upload_url" # 使用唯一临时文件避免跨 job 污染 asset_response_file=$(mktemp /tmp/asset_response_XXXXXX.json) trap "rm -f $release_response_file $asset_response_file" EXIT upload_success=false attempt=1 while [ $attempt -le $MAX_RETRIES ] && [ "$upload_success" = "false" ]; do echo "[Upload asset] Attempt $attempt/$MAX_RETRIES..." # 清空临时文件 > "$asset_response_file" # Gitea API 要求使用 multipart/form-data 格式上传文件 http_code=$(curl -sS -w "%{http_code}" -o "$asset_response_file" -X POST "$upload_url" \ -H "Authorization: token $GITEA_TOKEN" \ --connect-timeout 30 \ --max-time 300 \ -F "attachment=@$jar_file" 2>/dev/null) || http_code="000" if [ "$http_code" = "201" ]; then upload_success=true echo "✓ Successfully uploaded: $asset_name" else echo "✗ Upload failed (HTTP $http_code)" cat "$asset_response_file" 2>/dev/null || true fi if [ "$upload_success" = "false" ] && [ $attempt -lt $MAX_RETRIES ]; then echo "Retrying in ${RETRY_DELAY}s..." sleep $RETRY_DELAY fi attempt=$((attempt + 1)) done if [ "$upload_success" = "false" ]; then echo "✗ Failed to upload asset after $MAX_RETRIES attempts" exit 1 fi - name: Mark deployment success id: set_status if: always() run: | echo "status=success" >> $GITHUB_OUTPUT notify-on-failure: runs-on: act_runner_java needs: build-deploy if: ${{ always() && github.event.pull_request.merged == true && needs.build-deploy.result == 'failure' }} steps: - name: Notify CI failure env: PR_NUMBER: ${{ github.event.number }} PR_TITLE: ${{ github.event.pull_request.title }} PR_URL: ${{ github.event.pull_request.html_url }} SOURCE_BRANCH: ${{ github.event.pull_request.head.ref }} AUTHOR: ${{ github.event.pull_request.user.login }} COMMIT_SHA: ${{ github.sha }} REPO: ${{ github.repository }} SERVER_URL: ${{ github.server_url }} # 通知配置(按需启用) WEBHOOK_URL: ${{ vars.NOTIFY_WEBHOOK_URL }} run: | echo "=========================================" echo "CI Pipeline Failed - Manual Review Required" echo "=========================================" echo "" echo "PR: #$PR_NUMBER - $PR_TITLE" echo "Branch: $SOURCE_BRANCH" echo "Author: $AUTHOR" echo "Commit: $COMMIT_SHA" echo "" echo "Actions:" echo " 1. Re-run CI: $SERVER_URL/$REPO/actions" echo " 2. Revert PR: $PR_URL (click 'Revert' button)" echo "" echo "=========================================" # 发送 Webhook 通知(钉钉/企业微信/Slack 等) if [ -n "$WEBHOOK_URL" ]; then message="🚨 CI 部署失败\n\nPR: #$PR_NUMBER - $PR_TITLE\n分支: $SOURCE_BRANCH\n提交者: $AUTHOR\n\n请检查并决定:\n• 重试 CI\n• 回滚合并" # 通用 JSON 格式(适配大多数 Webhook) payload=$(cat <