Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fdb1363fb | |||
| 9030b0587e |
+401
-29
@@ -84,6 +84,13 @@ jobs:
|
|||||||
python3 -m pip --version
|
python3 -m pip --version
|
||||||
echo "CI environment is ready"
|
echo "CI environment is ready"
|
||||||
|
|
||||||
|
- name: Cache pip dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pip
|
||||||
|
key: pip-${{ runner.os }}-${{ hashFiles('requirements-base.txt', 'requirements.txt', 'requirements-dev.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
pip-${{ runner.os }}-
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
@@ -213,6 +220,13 @@ jobs:
|
|||||||
tar.extract(member, '.')
|
tar.extract(member, '.')
|
||||||
PY
|
PY
|
||||||
|
|
||||||
|
- name: Cache pip dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pip
|
||||||
|
key: pip-${{ runner.os }}-${{ hashFiles('requirements-base.txt', 'requirements.txt', 'requirements-dev.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
pip-${{ runner.os }}-
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
@@ -312,6 +326,13 @@ jobs:
|
|||||||
python3 -m pip --version
|
python3 -m pip --version
|
||||||
echo "CI environment is ready"
|
echo "CI environment is ready"
|
||||||
|
|
||||||
|
- name: Cache pip dependencies
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cache/pip
|
||||||
|
key: pip-${{ runner.os }}-${{ hashFiles('requirements-base.txt', 'requirements.txt', 'requirements-dev.txt') }}
|
||||||
|
restore-keys: |
|
||||||
|
pip-${{ runner.os }}-
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
@@ -587,14 +608,17 @@ jobs:
|
|||||||
echo "=== CI 失败通知 ==="
|
echo "=== CI 失败通知 ==="
|
||||||
FAILED_JOB="Frontend Lint" python3 scripts/ci_notify_failure.py
|
FAILED_JOB="Frontend Lint" python3 scripts/ci_notify_failure.py
|
||||||
|
|
||||||
deploy-staging:
|
build-staging-api:
|
||||||
name: Build & Push Staging (Watchtower auto-deploy)
|
name: Build Staging API Image
|
||||||
runs-on: saas
|
runs-on: saas
|
||||||
timeout-minutes: 30
|
timeout-minutes: 25
|
||||||
needs: [validate, frontend-lint]
|
needs: [validate, frontend-lint]
|
||||||
|
|
||||||
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
|
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_ONLY: api
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
shell: sh
|
shell: sh
|
||||||
@@ -641,16 +665,195 @@ jobs:
|
|||||||
tar.extract(member, '.')
|
tar.extract(member, '.')
|
||||||
INNERPY
|
INNERPY
|
||||||
|
|
||||||
- name: Build and push all images to Gitea Registry
|
|
||||||
|
- name: Build and push api image
|
||||||
shell: sh
|
shell: sh
|
||||||
env:
|
env:
|
||||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
REGISTRY_TOKEN: ${ secrets.REGISTRY_TOKEN }
|
||||||
|
ALLOW_SHARED_PRODUCTION_BUILD_HOST: "true"
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
chmod +x scripts/build_release_images.sh
|
chmod +x scripts/build_release_images.sh
|
||||||
ALLOW_SHARED_PRODUCTION_BUILD_HOST=true REGISTRY_TOKEN="${REGISTRY_TOKEN}" \
|
BUILD_ONLY="${BUILD_ONLY}" \
|
||||||
|
ALLOW_SHARED_PRODUCTION_BUILD_HOST=true \
|
||||||
|
REGISTRY_TOKEN="${REGISTRY_TOKEN}" \
|
||||||
scripts/build_release_images.sh "${GITHUB_SHA}" staging
|
scripts/build_release_images.sh "${GITHUB_SHA}" staging
|
||||||
|
|
||||||
|
- name: Notify CI failure
|
||||||
|
if: failure()
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
echo "=== CI 失败通知 ==="
|
||||||
|
FAILED_JOB="Build Staging API Image" python3 scripts/ci_notify_failure.py
|
||||||
|
|
||||||
|
build-staging-worker:
|
||||||
|
name: Build Staging Worker Image
|
||||||
|
runs-on: saas
|
||||||
|
timeout-minutes: 25
|
||||||
|
needs: [validate, frontend-lint]
|
||||||
|
|
||||||
|
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_ONLY: worker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
python3 - <<'INNERPY'
|
||||||
|
import io, os, tarfile, time, urllib.request, urllib.error
|
||||||
|
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
||||||
|
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
||||||
|
last_err = None
|
||||||
|
for attempt in range(5):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
|
archive = response.read()
|
||||||
|
break
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
last_err = e
|
||||||
|
if e.code >= 500 and attempt < 4:
|
||||||
|
wait = 2 ** attempt
|
||||||
|
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
last_err = e
|
||||||
|
if attempt < 4:
|
||||||
|
wait = 2 ** attempt
|
||||||
|
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise last_err
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
||||||
|
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
|
||||||
|
for member in tar.getmembers():
|
||||||
|
name = member.name
|
||||||
|
if name == root_prefix[:-1]:
|
||||||
|
continue
|
||||||
|
if name.startswith(root_prefix):
|
||||||
|
member.name = name[len(root_prefix):]
|
||||||
|
if member.name:
|
||||||
|
tar.extract(member, '.')
|
||||||
|
INNERPY
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build and push worker image
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${ secrets.REGISTRY_TOKEN }
|
||||||
|
ALLOW_SHARED_PRODUCTION_BUILD_HOST: "true"
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
chmod +x scripts/build_release_images.sh
|
||||||
|
BUILD_ONLY="${BUILD_ONLY}" \
|
||||||
|
ALLOW_SHARED_PRODUCTION_BUILD_HOST=true \
|
||||||
|
REGISTRY_TOKEN="${REGISTRY_TOKEN}" \
|
||||||
|
scripts/build_release_images.sh "${GITHUB_SHA}" staging
|
||||||
|
|
||||||
|
- name: Notify CI failure
|
||||||
|
if: failure()
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
echo "=== CI 失败通知 ==="
|
||||||
|
FAILED_JOB="Build Staging Worker Image" python3 scripts/ci_notify_failure.py
|
||||||
|
|
||||||
|
build-staging-web:
|
||||||
|
name: Build Staging Web Image
|
||||||
|
runs-on: saas
|
||||||
|
timeout-minutes: 25
|
||||||
|
needs: [validate, frontend-lint]
|
||||||
|
|
||||||
|
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_ONLY: web
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
python3 - <<'INNERPY'
|
||||||
|
import io, os, tarfile, time, urllib.request, urllib.error
|
||||||
|
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
||||||
|
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
||||||
|
last_err = None
|
||||||
|
for attempt in range(5):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
|
archive = response.read()
|
||||||
|
break
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
last_err = e
|
||||||
|
if e.code >= 500 and attempt < 4:
|
||||||
|
wait = 2 ** attempt
|
||||||
|
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
last_err = e
|
||||||
|
if attempt < 4:
|
||||||
|
wait = 2 ** attempt
|
||||||
|
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise last_err
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
||||||
|
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
|
||||||
|
for member in tar.getmembers():
|
||||||
|
name = member.name
|
||||||
|
if name == root_prefix[:-1]:
|
||||||
|
continue
|
||||||
|
if name.startswith(root_prefix):
|
||||||
|
member.name = name[len(root_prefix):]
|
||||||
|
if member.name:
|
||||||
|
tar.extract(member, '.')
|
||||||
|
INNERPY
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build and push web image
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${ secrets.REGISTRY_TOKEN }
|
||||||
|
ALLOW_SHARED_PRODUCTION_BUILD_HOST: "true"
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
chmod +x scripts/build_release_images.sh
|
||||||
|
BUILD_ONLY="${BUILD_ONLY}" \
|
||||||
|
ALLOW_SHARED_PRODUCTION_BUILD_HOST=true \
|
||||||
|
REGISTRY_TOKEN="${REGISTRY_TOKEN}" \
|
||||||
|
scripts/build_release_images.sh "${GITHUB_SHA}" staging
|
||||||
|
|
||||||
|
- name: Notify CI failure
|
||||||
|
if: failure()
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
echo "=== CI 失败通知 ==="
|
||||||
|
FAILED_JOB="Build Staging Web Image" python3 scripts/ci_notify_failure.py
|
||||||
|
|
||||||
|
deploy-staging:
|
||||||
|
name: Deploy Staging (Watchtower auto-deploy)
|
||||||
|
runs-on: saas
|
||||||
|
timeout-minutes: 20
|
||||||
|
needs: [build-staging-api, build-staging-worker, build-staging-web]
|
||||||
|
|
||||||
|
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'develop')
|
||||||
|
|
||||||
|
steps:
|
||||||
- name: Tag and push :staging images (Watchtower auto-update)
|
- name: Tag and push :staging images (Watchtower auto-update)
|
||||||
shell: sh
|
shell: sh
|
||||||
env:
|
env:
|
||||||
@@ -662,6 +865,7 @@ jobs:
|
|||||||
printf '%s' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin 2>/dev/null
|
printf '%s' "${REGISTRY_TOKEN}" | docker login git.xiaoxiajianji.com -u xiaoxia --password-stdin 2>/dev/null
|
||||||
fi
|
fi
|
||||||
for svc in api worker web; do
|
for svc in api worker web; do
|
||||||
|
docker pull "${REGISTRY}/xiaoxia-saas-${svc}:${GITHUB_SHA}"
|
||||||
docker tag "${REGISTRY}/xiaoxia-saas-${svc}:${GITHUB_SHA}" "${REGISTRY}/xiaoxia-saas-${svc}:staging"
|
docker tag "${REGISTRY}/xiaoxia-saas-${svc}:${GITHUB_SHA}" "${REGISTRY}/xiaoxia-saas-${svc}:staging"
|
||||||
docker push "${REGISTRY}/xiaoxia-saas-${svc}:staging"
|
docker push "${REGISTRY}/xiaoxia-saas-${svc}:staging"
|
||||||
done
|
done
|
||||||
@@ -732,9 +936,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
echo "=== CI 失败通知 ==="
|
echo "=== CI 失败通知 ==="
|
||||||
FAILED_JOB="Build & Push Staging (Watchtower auto-deploy)" python3 scripts/ci_notify_failure.py
|
FAILED_JOB="Deploy Staging" python3 scripts/ci_notify_failure.py
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
staging-e2e:
|
staging-e2e:
|
||||||
name: Staging E2E Tests
|
name: Staging E2E Tests
|
||||||
@@ -887,14 +1089,17 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
build-production-runtime-images:
|
build-prod-api:
|
||||||
name: Build Production Runtime Images
|
name: Build Production API Image
|
||||||
runs-on: saas
|
runs-on: saas
|
||||||
timeout-minutes: 30
|
timeout-minutes: 25
|
||||||
needs: [validate, frontend-lint]
|
needs: [validate, frontend-lint]
|
||||||
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_ONLY: api
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
shell: sh
|
shell: sh
|
||||||
@@ -902,7 +1107,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
python3 - <<'PY'
|
python3 - <<'INNERPY'
|
||||||
import io, os, tarfile, time, urllib.request, urllib.error
|
import io, os, tarfile, time, urllib.request, urllib.error
|
||||||
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
||||||
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
||||||
@@ -927,7 +1132,6 @@ jobs:
|
|||||||
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||||
time.sleep(wait)
|
time.sleep(wait)
|
||||||
continue
|
continue
|
||||||
raise
|
|
||||||
else:
|
else:
|
||||||
raise last_err
|
raise last_err
|
||||||
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
||||||
@@ -940,47 +1144,215 @@ jobs:
|
|||||||
member.name = name[len(root_prefix):]
|
member.name = name[len(root_prefix):]
|
||||||
if member.name:
|
if member.name:
|
||||||
tar.extract(member, '.')
|
tar.extract(member, '.')
|
||||||
PY
|
INNERPY
|
||||||
|
|
||||||
- name: Build and push all images (api + worker + web, with buildx cache)
|
|
||||||
|
- name: Build and push production api image
|
||||||
shell: sh
|
shell: sh
|
||||||
env:
|
env:
|
||||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
REGISTRY_TOKEN: ${ secrets.REGISTRY_TOKEN }
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
chmod +x scripts/build_release_images.sh
|
chmod +x scripts/build_release_images.sh
|
||||||
REGISTRY_TOKEN="${REGISTRY_TOKEN}" scripts/build_release_images.sh "${GITHUB_REF_NAME}"
|
BUILD_ONLY="${BUILD_ONLY}" \
|
||||||
|
REGISTRY_TOKEN="${REGISTRY_TOKEN}" \
|
||||||
|
scripts/build_release_images.sh "${GITHUB_REF_NAME}"
|
||||||
|
|
||||||
|
|
||||||
- name: Cleanup old Docker images
|
- name: Cleanup old Docker images
|
||||||
if: always()
|
if: always()
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set +e
|
||||||
if [ -f scripts/cleanup_old_images.sh ]; then
|
docker image prune -f 2>/dev/null || true
|
||||||
chmod +x scripts/cleanup_old_images.sh
|
|
||||||
scripts/cleanup_old_images.sh
|
|
||||||
else
|
|
||||||
echo "Cleanup script not found, doing basic prune..."
|
|
||||||
docker image prune -f 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
echo "Disk usage after cleanup:"
|
echo "Disk usage after cleanup:"
|
||||||
df -h / | tail -1
|
df -h / | tail -1
|
||||||
|
|
||||||
- name: Notify CI failure
|
- name: Notify CI failure
|
||||||
if: failure()
|
if: failure()
|
||||||
shell: sh
|
shell: sh
|
||||||
run: |
|
run: |
|
||||||
set +e
|
set +e
|
||||||
echo "=== CI 失败通知 ==="
|
echo "=== CI 失败通知 ==="
|
||||||
FAILED_JOB="Build Production Runtime Images" python3 scripts/ci_notify_failure.py
|
FAILED_JOB="Build Production API Image" python3 scripts/ci_notify_failure.py
|
||||||
|
|
||||||
|
build-prod-worker:
|
||||||
|
name: Build Production Worker Image
|
||||||
|
runs-on: saas
|
||||||
|
timeout-minutes: 25
|
||||||
|
needs: [validate, frontend-lint]
|
||||||
|
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_ONLY: worker
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
python3 - <<'INNERPY'
|
||||||
|
import io, os, tarfile, time, urllib.request, urllib.error
|
||||||
|
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
||||||
|
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
||||||
|
last_err = None
|
||||||
|
for attempt in range(5):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
|
archive = response.read()
|
||||||
|
break
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
last_err = e
|
||||||
|
if e.code >= 500 and attempt < 4:
|
||||||
|
wait = 2 ** attempt
|
||||||
|
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
last_err = e
|
||||||
|
if attempt < 4:
|
||||||
|
wait = 2 ** attempt
|
||||||
|
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise last_err
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
||||||
|
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
|
||||||
|
for member in tar.getmembers():
|
||||||
|
name = member.name
|
||||||
|
if name == root_prefix[:-1]:
|
||||||
|
continue
|
||||||
|
if name.startswith(root_prefix):
|
||||||
|
member.name = name[len(root_prefix):]
|
||||||
|
if member.name:
|
||||||
|
tar.extract(member, '.')
|
||||||
|
INNERPY
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build and push production worker image
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${ secrets.REGISTRY_TOKEN }
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
chmod +x scripts/build_release_images.sh
|
||||||
|
BUILD_ONLY="${BUILD_ONLY}" \
|
||||||
|
REGISTRY_TOKEN="${REGISTRY_TOKEN}" \
|
||||||
|
scripts/build_release_images.sh "${GITHUB_REF_NAME}"
|
||||||
|
|
||||||
|
|
||||||
|
- name: Cleanup old Docker images
|
||||||
|
if: always()
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
docker image prune -f 2>/dev/null || true
|
||||||
|
echo "Disk usage after cleanup:"
|
||||||
|
df -h / | tail -1
|
||||||
|
- name: Notify CI failure
|
||||||
|
if: failure()
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
echo "=== CI 失败通知 ==="
|
||||||
|
FAILED_JOB="Build Production Worker Image" python3 scripts/ci_notify_failure.py
|
||||||
|
|
||||||
|
build-prod-web:
|
||||||
|
name: Build Production Web Image
|
||||||
|
runs-on: saas
|
||||||
|
timeout-minutes: 25
|
||||||
|
needs: [validate, frontend-lint]
|
||||||
|
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILD_ONLY: web
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
python3 - <<'INNERPY'
|
||||||
|
import io, os, tarfile, time, urllib.request, urllib.error
|
||||||
|
url = f"{os.environ['GITHUB_API_URL']}/repos/{os.environ['GITHUB_REPOSITORY']}/archive/{os.environ['GITHUB_SHA']}.tar.gz"
|
||||||
|
request = urllib.request.Request(url, headers={"Authorization": f"token {os.environ['GITHUB_TOKEN']}"})
|
||||||
|
last_err = None
|
||||||
|
for attempt in range(5):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
|
archive = response.read()
|
||||||
|
break
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
last_err = e
|
||||||
|
if e.code >= 500 and attempt < 4:
|
||||||
|
wait = 2 ** attempt
|
||||||
|
print(f"Checkout HTTP {e.code}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
last_err = e
|
||||||
|
if attempt < 4:
|
||||||
|
wait = 2 ** attempt
|
||||||
|
print(f"Checkout error: {e}, retrying in {wait}s (attempt {attempt+1}/5)...")
|
||||||
|
time.sleep(wait)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise last_err
|
||||||
|
with tarfile.open(fileobj=io.BytesIO(archive), mode='r:gz') as tar:
|
||||||
|
root_prefix = tar.getmembers()[0].name.split('/', 1)[0] + '/'
|
||||||
|
for member in tar.getmembers():
|
||||||
|
name = member.name
|
||||||
|
if name == root_prefix[:-1]:
|
||||||
|
continue
|
||||||
|
if name.startswith(root_prefix):
|
||||||
|
member.name = name[len(root_prefix):]
|
||||||
|
if member.name:
|
||||||
|
tar.extract(member, '.')
|
||||||
|
INNERPY
|
||||||
|
|
||||||
|
|
||||||
|
- name: Build and push production web image
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${ secrets.REGISTRY_TOKEN }
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
chmod +x scripts/build_release_images.sh
|
||||||
|
BUILD_ONLY="${BUILD_ONLY}" \
|
||||||
|
REGISTRY_TOKEN="${REGISTRY_TOKEN}" \
|
||||||
|
scripts/build_release_images.sh "${GITHUB_REF_NAME}"
|
||||||
|
|
||||||
|
|
||||||
|
- name: Cleanup old Docker images
|
||||||
|
if: always()
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
docker image prune -f 2>/dev/null || true
|
||||||
|
echo "Disk usage after cleanup:"
|
||||||
|
df -h / | tail -1
|
||||||
|
- name: Notify CI failure
|
||||||
|
if: failure()
|
||||||
|
shell: sh
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
echo "=== CI 失败通知 ==="
|
||||||
|
FAILED_JOB="Build Production Web Image" python3 scripts/ci_notify_failure.py
|
||||||
|
|
||||||
deploy-production:
|
deploy-production:
|
||||||
name: Deploy Production
|
name: Deploy Production
|
||||||
runs-on: saas
|
runs-on: saas
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: build-production-runtime-images
|
needs: [build-prod-api, build-prod-worker, build-prod-web]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install SSH client
|
- name: Install SSH client
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ case "$BUILD_ENV" in
|
|||||||
esac
|
esac
|
||||||
echo "Build environment: $BUILD_ENV → nginx config: $NGINX_CONF_FILE"
|
echo "Build environment: $BUILD_ENV → nginx config: $NGINX_CONF_FILE"
|
||||||
|
|
||||||
|
# 可选:只构建指定镜像(api|worker|web),空则全部构建
|
||||||
|
BUILD_ONLY="${BUILD_ONLY:-}"
|
||||||
|
if [ -n "$BUILD_ONLY" ]; then
|
||||||
|
echo "Build mode: only $BUILD_ONLY"
|
||||||
|
fi
|
||||||
|
|
||||||
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
cd "$ROOT_DIR"
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
@@ -60,26 +66,22 @@ fi
|
|||||||
# 默认只读不写,防止 feature 分支污染主缓存
|
# 默认只读不写,防止 feature 分支污染主缓存
|
||||||
# 只有 develop/main 分支才写回缓存
|
# 只有 develop/main 分支才写回缓存
|
||||||
BRANCH_NAME="${GITHUB_REF_NAME:-${CI_COMMIT_BRANCH:-unknown}}"
|
BRANCH_NAME="${GITHUB_REF_NAME:-${CI_COMMIT_BRANCH:-unknown}}"
|
||||||
if [ "$USE_CACHE" -eq 1 ]; then
|
case "$BRANCH_NAME" in
|
||||||
docker buildx build \
|
develop|main) CACHE_WRITE=1 ; echo "Cache mode: read+write (branch: $BRANCH_NAME)" ;;
|
||||||
--build-arg APP_VERSION="$VERSION" \
|
*) CACHE_WRITE=0 ; echo "Cache mode: read-only (branch: $BRANCH_NAME)" ;;
|
||||||
--cache-from "type=registry,ref=${CACHE_REGISTRY}/api-cache:${CACHE_TAG},ignore-error=true" \
|
esac
|
||||||
--cache-to "type=registry,ref=${CACHE_REGISTRY}/api-cache:${CACHE_TAG},mode=max" \
|
|
||||||
-f infra/docker/api.Dockerfile \
|
|
||||||
-t "$API_IMAGE" -t "$API_LATEST" \
|
|
||||||
--load \
|
|
||||||
.
|
|
||||||
else
|
|
||||||
docker build --pull=false --build-arg APP_VERSION="$VERSION" -f infra/docker/api.Dockerfile -t "$API_IMAGE" -t "$API_LATEST" .
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
# ---- 构建函数:使用 buildx 直接 push 到 registry ----
|
||||||
|
# 不再使用 --load → docker tag → docker push 的串行方式
|
||||||
|
# buildx --push 直接从 buildkit 推送,省去序列化导入 daemon 的开销
|
||||||
build_with_cache() {
|
build_with_cache() {
|
||||||
# usage: build_with_cache <image_name> <dockerfile> <extra_args...>
|
# usage: build_with_cache <image_name> <dockerfile> [extra_args...]
|
||||||
IMG_NAME="$1"
|
IMG_NAME="$1"
|
||||||
DOCKERFILE="$2"
|
DOCKERFILE="$2"
|
||||||
shift 2
|
shift 2
|
||||||
EXTRA_ARGS="$*"
|
EXTRA_ARGS="$*"
|
||||||
|
|
||||||
|
REGISTRY_IMG="${REGISTRY}/xiaoxia-saas-${IMG_NAME}:$VERSION"
|
||||||
CACHE_FROM="type=registry,ref=${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY},ignore-error=true"
|
CACHE_FROM="type=registry,ref=${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY},ignore-error=true"
|
||||||
|
|
||||||
if [ "$CACHE_WRITE" -eq 1 ]; then
|
if [ "$CACHE_WRITE" -eq 1 ]; then
|
||||||
@@ -90,96 +92,100 @@ build_with_cache() {
|
|||||||
echo " cache: read-only from ${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY}"
|
echo " cache: read-only from ${CACHE_REGISTRY}/${IMG_NAME}-cache:${CACHE_TAG_PRIMARY}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$USE_CACHE" -eq 1 ]; then
|
if [ "$USE_CACHE" -eq 1 ] && [ "$USE_PUSH" -eq 1 ]; then
|
||||||
if [ -n "$CACHE_TO" ]; then
|
if [ -n "$CACHE_TO" ]; then
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
$EXTRA_ARGS \
|
$EXTRA_ARGS \
|
||||||
--cache-from "$CACHE_FROM" \
|
--cache-from "$CACHE_FROM" \
|
||||||
--cache-to "$CACHE_TO" \
|
--cache-to "$CACHE_TO" \
|
||||||
-f "$DOCKERFILE" \
|
-f "$DOCKERFILE" \
|
||||||
-t "$IMG_NAME:$VERSION" \
|
-t "$REGISTRY_IMG" \
|
||||||
--load \
|
--push \
|
||||||
.
|
.
|
||||||
else
|
else
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
$EXTRA_ARGS \
|
$EXTRA_ARGS \
|
||||||
--cache-from "$CACHE_FROM" \
|
--cache-from "$CACHE_FROM" \
|
||||||
-f "$DOCKERFILE" \
|
-f "$DOCKERFILE" \
|
||||||
-t "$IMG_NAME:$VERSION" \
|
-t "$REGISTRY_IMG" \
|
||||||
--load \
|
--push \
|
||||||
.
|
.
|
||||||
fi
|
fi
|
||||||
|
echo " ✅ Pushed: $REGISTRY_IMG"
|
||||||
else
|
else
|
||||||
docker build --pull=false $EXTRA_ARGS -f "$DOCKERFILE" -t "$IMG_NAME:$VERSION" .
|
# 无push权限时,本地构建用于测试
|
||||||
|
docker build --pull=false $EXTRA_ARGS -f "$DOCKERFILE" -t "xiaoxia-saas-${IMG_NAME}:$VERSION" .
|
||||||
|
echo " ✅ Built locally: xiaoxia-saas-${IMG_NAME}:$VERSION"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "=== Building API image ==="
|
should_build() {
|
||||||
build_with_cache "api" "infra/docker/api.Dockerfile" \
|
# usage: should_build <image_name>
|
||||||
"--build-arg APP_VERSION=$VERSION"
|
# 返回0表示需要构建,1表示跳过
|
||||||
docker tag "$API_IMAGE" "$API_LATEST"
|
local img="$1"
|
||||||
|
if [ -z "$BUILD_ONLY" ]; then
|
||||||
|
return 0 # 全部构建
|
||||||
|
fi
|
||||||
|
if [ "$BUILD_ONLY" = "$img" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
echo "=== Building Worker image ==="
|
if should_build "api"; then
|
||||||
if [ "$USE_CACHE" -eq 1 ]; then
|
echo ""
|
||||||
docker buildx build \
|
echo "=== Building API image ==="
|
||||||
--build-arg APP_VERSION="$VERSION" \
|
build_with_cache "api" "infra/docker/api.Dockerfile" \
|
||||||
--cache-from "type=registry,ref=${CACHE_REGISTRY}/worker-cache:${CACHE_TAG},ignore-error=true" \
|
"--build-arg APP_VERSION=$VERSION"
|
||||||
--cache-to "type=registry,ref=${CACHE_REGISTRY}/worker-cache:${CACHE_TAG},mode=max" \
|
|
||||||
-f infra/docker/worker.Dockerfile \
|
# 本地也打一个 :dev 标签方便本地引用(如果有本地镜像的话)
|
||||||
-t "$WORKER_IMAGE" -t "$WORKER_LATEST" \
|
if [ "$USE_PUSH" -eq 0 ]; then
|
||||||
--load \
|
docker tag "$API_IMAGE" "$API_LATEST" 2>/dev/null || true
|
||||||
.
|
fi
|
||||||
else
|
|
||||||
docker build --pull=false --build-arg APP_VERSION="$VERSION" -f infra/docker/worker.Dockerfile -t "$WORKER_IMAGE" -t "$WORKER_LATEST" .
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "=== Building Web image (with buildx cache) ==="
|
if should_build "worker"; then
|
||||||
# 先构建前端产物(使用持久化 npm 缓存卷)
|
echo ""
|
||||||
NPM_CACHE_VOLUME="xiaoxia-npm-cache"
|
echo "=== Building Worker image ==="
|
||||||
if ! docker volume inspect "$NPM_CACHE_VOLUME" >/dev/null 2>&1; then
|
build_with_cache "worker" "infra/docker/worker.Dockerfile" \
|
||||||
docker volume create "$NPM_CACHE_VOLUME" >/dev/null
|
"--build-arg APP_VERSION=$VERSION"
|
||||||
echo " Created npm cache volume: $NPM_CACHE_VOLUME"
|
|
||||||
|
if [ "$USE_PUSH" -eq 0 ]; then
|
||||||
|
docker tag "$WORKER_IMAGE" "$WORKER_LATEST" 2>/dev/null || true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker run --rm \
|
if should_build "web"; then
|
||||||
-v "$PWD:/workspace" \
|
echo ""
|
||||||
-v "$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules" \
|
echo "=== Building Web image ==="
|
||||||
-w /workspace/apps/web \
|
# 先构建前端产物(使用持久化 npm 缓存卷)
|
||||||
docker.m.daocloud.io/library/node:20 \
|
NPM_CACHE_VOLUME="xiaoxia-npm-cache"
|
||||||
sh -lc "npm ci && npm run build"
|
if ! docker volume inspect "$NPM_CACHE_VOLUME" >/dev/null 2>&1; then
|
||||||
|
docker volume create "$NPM_CACHE_VOLUME" >/dev/null
|
||||||
|
echo " Created npm cache volume: $NPM_CACHE_VOLUME"
|
||||||
|
fi
|
||||||
|
|
||||||
test -f apps/web/dist/index.html
|
docker run --rm \
|
||||||
|
-v "$PWD:/workspace" \
|
||||||
|
-v "$NPM_CACHE_VOLUME:/workspace/apps/web/node_modules" \
|
||||||
|
-w /workspace/apps/web \
|
||||||
|
docker.m.daocloud.io/library/node:20 \
|
||||||
|
sh -lc "npm ci && npm run build"
|
||||||
|
|
||||||
if [ "$USE_CACHE" -eq 1 ]; then
|
test -f apps/web/dist/index.html
|
||||||
docker buildx build \
|
|
||||||
--cache-from "type=registry,ref=${CACHE_REGISTRY}/web-cache:${CACHE_TAG},ignore-error=true" \
|
|
||||||
--cache-to "type=registry,ref=${CACHE_REGISTRY}/web-cache:${CACHE_TAG},mode=max" \
|
|
||||||
-f infra/docker/web-artifact.Dockerfile \
|
|
||||||
--build-arg "NGINX_CONF=$NGINX_CONF_FILE" \
|
|
||||||
-t "$WEB_IMAGE" \
|
|
||||||
--load \
|
|
||||||
.
|
|
||||||
else
|
|
||||||
docker build --pull=false \
|
|
||||||
-f infra/docker/web-artifact.Dockerfile \
|
|
||||||
--build-arg "NGINX_CONF=$NGINX_CONF_FILE" \
|
|
||||||
-t "$WEB_IMAGE" \
|
|
||||||
.
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Push 到 Registry
|
build_with_cache "web" "infra/docker/web-artifact.Dockerfile" \
|
||||||
if [ "$USE_PUSH" -eq 1 ]; then
|
"--build-arg NGINX_CONF=$NGINX_CONF_FILE"
|
||||||
echo "=== Pushing images to Registry ==="
|
|
||||||
docker tag "$API_IMAGE" "$REGISTRY_API"
|
|
||||||
docker tag "$WORKER_IMAGE" "$REGISTRY_WORKER"
|
|
||||||
docker tag "$WEB_IMAGE" "$REGISTRY_WEB"
|
|
||||||
docker push "$REGISTRY_API"
|
|
||||||
docker push "$REGISTRY_WORKER"
|
|
||||||
docker push "$REGISTRY_WEB"
|
|
||||||
echo "All images pushed to $REGISTRY"
|
|
||||||
else
|
|
||||||
echo "Registry push skipped (no auth token available)"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
echo "=== Build complete ==="
|
echo "=== Build complete ==="
|
||||||
docker images | grep "xiaoxia-saas.*:$VERSION"
|
if [ "$USE_PUSH" -eq 1 ]; then
|
||||||
|
echo "Images pushed to $REGISTRY:"
|
||||||
|
should_build "api" && echo " - $REGISTRY_API"
|
||||||
|
should_build "worker" && echo " - $REGISTRY_WORKER"
|
||||||
|
should_build "web" && echo " - $REGISTRY_WEB"
|
||||||
|
else
|
||||||
|
echo "Images built locally (registry push skipped)"
|
||||||
|
docker images | grep "xiaoxia-saas.*:$VERSION" || true
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user