feat(deploy): 接入全自动生产发布校验
This commit is contained in:
148
.github/workflows/deploy-production.yml
vendored
Normal file
148
.github/workflows/deploy-production.yml
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
name: Deploy Production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: production
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
NEXT_PUBLIC_API_URL: /api/v1
|
||||
NEXT_API_PROXY_TARGET: http://server:3001
|
||||
|
||||
jobs:
|
||||
build-push-deploy:
|
||||
name: Build, push, deploy, verify
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Prepare image metadata
|
||||
id: meta
|
||||
shell: bash
|
||||
run: |
|
||||
repo_lc="${GITHUB_REPOSITORY,,}"
|
||||
build_time="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||||
echo "repo_lc=${repo_lc}" >> "$GITHUB_OUTPUT"
|
||||
echo "build_time=${build_time}" >> "$GITHUB_OUTPUT"
|
||||
echo "web_image=${REGISTRY}/${repo_lc}/web:${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "server_image=${REGISTRY}/${repo_lc}/server:${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "web_image_master=${REGISTRY}/${repo_lc}/web:master" >> "$GITHUB_OUTPUT"
|
||||
echo "server_image_master=${REGISTRY}/${repo_lc}/server:master" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push web image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.web
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.web_image }}
|
||||
${{ steps.meta.outputs.web_image_master }}
|
||||
build-args: |
|
||||
NEXT_PUBLIC_API_URL=${{ env.NEXT_PUBLIC_API_URL }}
|
||||
NEXT_API_PROXY_TARGET=${{ env.NEXT_API_PROXY_TARGET }}
|
||||
APP_VERSION=${{ github.sha }}
|
||||
APP_BUILD_TIME=${{ steps.meta.outputs.build_time }}
|
||||
APP_IMAGE_TAG=${{ steps.meta.outputs.web_image }}
|
||||
|
||||
- name: Build and push server image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.server
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.server_image }}
|
||||
${{ steps.meta.outputs.server_image_master }}
|
||||
build-args: |
|
||||
APP_VERSION=${{ github.sha }}
|
||||
APP_BUILD_TIME=${{ steps.meta.outputs.build_time }}
|
||||
APP_IMAGE_TAG=${{ steps.meta.outputs.server_image }}
|
||||
|
||||
- name: Deploy over SSH
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.PROD_HOST }}
|
||||
username: ${{ secrets.PROD_USER }}
|
||||
key: ${{ secrets.PROD_SSH_KEY }}
|
||||
script_stop: true
|
||||
script: |
|
||||
set -euo pipefail
|
||||
|
||||
cd "${{ secrets.PROD_APP_DIR }}"
|
||||
test -f .env.production
|
||||
|
||||
git fetch origin master
|
||||
git checkout master
|
||||
git pull --ff-only origin master
|
||||
|
||||
set_env() {
|
||||
key="$1"
|
||||
value="$2"
|
||||
if grep -q "^${key}=" .env.production; then
|
||||
sed -i "s|^${key}=.*|${key}=${value}|" .env.production
|
||||
else
|
||||
printf "\n%s=%s\n" "${key}" "${value}" >> .env.production
|
||||
fi
|
||||
}
|
||||
|
||||
set_env APP_VERSION "${{ github.sha }}"
|
||||
set_env APP_BUILD_TIME "${{ steps.meta.outputs.build_time }}"
|
||||
set_env WEB_IMAGE "${{ steps.meta.outputs.web_image }}"
|
||||
set_env SERVER_IMAGE "${{ steps.meta.outputs.server_image }}"
|
||||
|
||||
if [ -n "${{ secrets.GHCR_READ_TOKEN }}" ]; then
|
||||
echo "${{ secrets.GHCR_READ_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
|
||||
fi
|
||||
|
||||
docker compose --env-file .env.production -f docker-compose.prod.yml pull web server
|
||||
docker compose --env-file .env.production -f docker-compose.prod.yml up -d postgres redis
|
||||
docker compose --env-file .env.production -f docker-compose.prod.yml run --rm -T server pnpm --filter server db:deploy
|
||||
docker compose --env-file .env.production -f docker-compose.prod.yml up -d --remove-orphans
|
||||
|
||||
for attempt in $(seq 1 30); do
|
||||
if docker compose --env-file .env.production -f docker-compose.prod.yml exec -T web node -e "
|
||||
const expected = process.argv[1];
|
||||
fetch('http://nginx/api/v1/health/version')
|
||||
.then(async (response) => {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
const payload = await response.json();
|
||||
if (payload.version !== expected) {
|
||||
throw new Error('Expected ' + expected + ', got ' + payload.version);
|
||||
}
|
||||
console.log('Runtime version verified: ' + payload.version);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
" "${{ github.sha }}"; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
echo "Runtime version check failed after retries"
|
||||
exit 1
|
||||
Reference in New Issue
Block a user