diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..3dc87fc --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,172 @@ +name: Build and Deploy VuePress + +on: + push: + branches: [ main ] + workflow_dispatch: + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build VuePress site + run: yarn docs:build + + - name: Get runner public IP + id: ip + run: echo "ipaddr=$(curl -s https://api.ipify.org)" >> $GITHUB_OUTPUT + + - name: Add IP to Aliyun Security Group + env: + ALIYUN_ACCESS_KEY_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }} + ALIYUN_ACCESS_KEY_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }} + ALIYUN_REGION: ${{ secrets.ALIYUN_REGION }} + ALIYUN_SECURITY_GROUP_ID: ${{ secrets.ALIYUN_SECURITY_GROUP_ID }} + SERVER_SSH_PORT: ${{ secrets.SERVER_PORT }} + RUNNER_IP: ${{ steps.ip.outputs.ipaddr }} + run: | + pip install aliyun-python-sdk-core aliyun-python-sdk-ecs + python3 << 'EOF' + import os + import sys + from aliyunsdkcore.client import AcsClient + from aliyunsdkecs.request.v20140526 import AuthorizeSecurityGroupRequest + + client = AcsClient( + os.environ['ALIYUN_ACCESS_KEY_ID'], + os.environ['ALIYUN_ACCESS_KEY_SECRET'], + os.environ['ALIYUN_REGION'] + ) + + request = AuthorizeSecurityGroupRequest.AuthorizeSecurityGroupRequest() + request.set_SecurityGroupId(os.environ['ALIYUN_SECURITY_GROUP_ID']) + request.set_IpProtocol('tcp') + request.set_PortRange(f"{os.environ['SERVER_SSH_PORT']}/{os.environ['SERVER_SSH_PORT']}") + request.set_SourceCidrIp(f"{os.environ['RUNNER_IP']}/32") + request.set_Description('GitHub Actions temporary access') + + try: + response = client.do_action_with_exception(request) + print(f"✓ Added {os.environ['RUNNER_IP']} to security group") + except Exception as e: + if 'InvalidPermission.Duplicate' in str(e): + print(f"✓ Rule already exists for {os.environ['RUNNER_IP']}") + else: + print(f"✗ Error: {e}") + sys.exit(1) + EOF + + - name: Deploy to Aliyun Server + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY }} + port: ${{ secrets.SERVER_PORT }} + source: "docs/.vuepress/dist/*" + target: ${{ secrets.DEPLOY_PATH }} + strip_components: 3 + overwrite: true + rm: true + + - name: Set Permissions + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY }} + port: ${{ secrets.SERVER_PORT }} + script: | + chmod -R 644 ${{ secrets.DEPLOY_PATH }} + chown -R ${{ secrets.SERVER_USER }}:${{ secrets.SERVER_USER }} ${{ secrets.DEPLOY_PATH }} + + - name: Remove IP from Aliyun Security Group + if: always() + env: + ALIYUN_ACCESS_KEY_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }} + ALIYUN_ACCESS_KEY_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }} + ALIYUN_REGION: ${{ secrets.ALIYUN_REGION }} + ALIYUN_SECURITY_GROUP_ID: ${{ secrets.ALIYUN_SECURITY_GROUP_ID }} + SERVER_SSH_PORT: ${{ secrets.SERVER_PORT }} + RUNNER_IP: ${{ steps.ip.outputs.ipaddr }} + run: | + pip install aliyun-python-sdk-core aliyun-python-sdk-ecs + python3 << 'EOF' + import os + from aliyunsdkcore.client import AcsClient + from aliyunsdkecs.request.v20140526 import RevokeSecurityGroupRequest + + client = AcsClient( + os.environ['ALIYUN_ACCESS_KEY_ID'], + os.environ['ALIYUN_ACCESS_KEY_SECRET'], + os.environ['ALIYUN_REGION'] + ) + + request = RevokeSecurityGroupRequest.RevokeSecurityGroupRequest() + request.set_SecurityGroupId(os.environ['ALIYUN_SECURITY_GROUP_ID']) + request.set_IpProtocol('tcp') + request.set_PortRange(f"{os.environ['SERVER_SSH_PORT']}/{os.environ['SERVER_SSH_PORT']}") + request.set_SourceCidrIp(f"{os.environ['RUNNER_IP']}/32") + + try: + response = client.do_action_with_exception(request) + print(f"✓ Removed {os.environ['RUNNER_IP']} from security group") + except Exception as e: + print(f"⚠ Warning: Failed to remove rule: {e}") + EOF + + - name: Notify Success to WeChat + if: success() + env: + COMMIT_MSG: ${{ github.event.head_commit.message }} + run: | + curl "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${{ secrets.WECHAT_WEBHOOK_KEY }}" \ + -H 'Content-Type: application/json' \ + -d "{ + \"msgtype\": \"markdown\", + \"markdown\": { + \"content\": \"## ✅ 部署成功\n> 项目:**Handbook (VuePress)**\n> 分支:\\\`${{ github.ref_name }}\\\`\n> 提交者:${{ github.actor }}\n> 提交信息:${COMMIT_MSG}\n> 状态:已成功部署到生产环境\" + } + }" + + - name: Get Job Logs + if: failure() + id: logs + run: | + ERROR_LOGS=$(cat $GITHUB_STEP_SUMMARY 2>/dev/null || echo "无法获取错误日志") + echo "logs<> $GITHUB_OUTPUT + echo "$ERROR_LOGS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Notify Failure to WeChat + if: failure() + env: + WORKFLOW_NAME: ${{ github.workflow }} + RUN_ID: ${{ github.run_id }} + JOB_NAME: ${{ github.job }} + run: | + FAILED_STEP=$(echo "${{ toJSON(job) }}" | jq -r '.steps[] | select(.conclusion=="failure") | .name' | head -1 || echo "未知步骤") + + curl "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${{ secrets.WECHAT_WEBHOOK_KEY }}" \ + -H 'Content-Type: application/json' \ + -d "{ + \"msgtype\": \"markdown\", + \"markdown\": { + \"content\": \"## ❌ 部署失败\n> 项目:**Handbook (VuePress)**\n> 分支:\\\`${{ github.ref_name }}\\\`\n> 提交者:${{ github.actor }}\n> 提交信息:${{ github.event.head_commit.message }}\n> 失败步骤:${FAILED_STEP}\n> Run ID:${RUN_ID}\n\n查看完整日志请前往 GitHub Actions\" + } + }" \ No newline at end of file