mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(ci,alembic): Hub Docker镜像构建发布,数据库迁移并发安全加固
- build-hub.yml 新增 Docker job,构建多架构镜像推送至 GHCR 和 Docker Hub - build-hub/build-proxy Release 名称简化为 tag 名 - alembic/env.py 使用 PostgreSQL advisory lock 防止多进程并发迁移竞态 - 迁移脚本改用 ADD/DROP COLUMN IF NOT EXISTS 替代 inspector 检查
This commit is contained in:
83
.github/workflows/build-hub.yml
vendored
83
.github/workflows/build-hub.yml
vendored
@@ -7,6 +7,12 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
GHCR_IMAGE: fawney19/aether-hub
|
||||||
|
DOCKERHUB_IMAGE: fawney19/aether-hub
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -83,9 +89,84 @@ jobs:
|
|||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
name: "aether-hub ${{ github.ref_name }}"
|
name: "${{ github.ref_name }}"
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: |
|
files: |
|
||||||
artifacts/aether-hub-*
|
artifacts/aether-hub-*
|
||||||
artifacts/SHA256SUMS.txt
|
artifacts/SHA256SUMS.txt
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
|
|
||||||
|
docker:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download Linux artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: aether-hub-linux-*
|
||||||
|
merge-multiple: true
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Prepare binaries
|
||||||
|
run: |
|
||||||
|
mkdir -p aether-hub/build/linux-amd64 aether-hub/build/linux-arm64
|
||||||
|
tar xzf artifacts/aether-hub-linux-amd64.tar.gz -C aether-hub/build/linux-amd64
|
||||||
|
tar xzf artifacts/aether-hub-linux-arm64.tar.gz -C aether-hub/build/linux-arm64
|
||||||
|
|
||||||
|
- name: Generate CI Dockerfile
|
||||||
|
run: |
|
||||||
|
cat > aether-hub/Dockerfile.ci << 'EOF'
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
ARG TARGETARCH
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY build/linux-${TARGETARCH}/aether-hub /usr/local/bin/aether-hub
|
||||||
|
EXPOSE 8085
|
||||||
|
ENTRYPOINT ["/usr/local/bin/aether-hub"]
|
||||||
|
CMD ["--bind", "0.0.0.0:8085"]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- 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: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.GHCR_IMAGE }}
|
||||||
|
docker.io/${{ env.DOCKERHUB_IMAGE }}
|
||||||
|
tags: |
|
||||||
|
type=match,pattern=hub-v(.*),group=1
|
||||||
|
type=match,pattern=hub-v(\d+\.\d+),group=1
|
||||||
|
type=sha,prefix=
|
||||||
|
flavor: |
|
||||||
|
latest=auto
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./aether-hub
|
||||||
|
file: ./aether-hub/Dockerfile.ci
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
2
.github/workflows/build-proxy.yml
vendored
2
.github/workflows/build-proxy.yml
vendored
@@ -113,7 +113,7 @@ jobs:
|
|||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
name: "aether-proxy ${{ github.ref_name }}"
|
name: "${{ github.ref_name }}"
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
files: |
|
files: |
|
||||||
artifacts/aether-proxy-*
|
artifacts/aether-proxy-*
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ Alembic 环境配置
|
|||||||
用于数据库迁移的运行时环境设置
|
用于数据库迁移的运行时环境设置
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from logging.config import fileConfig
|
|
||||||
from sqlalchemy import engine_from_config, pool
|
|
||||||
from alembic import context
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from logging.config import fileConfig
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from sqlalchemy import engine_from_config, pool, text
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
|
||||||
# 添加项目根目录到 Python 路径
|
# 添加项目根目录到 Python 路径
|
||||||
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
|
||||||
@@ -48,6 +50,10 @@ if config.config_file_name is not None:
|
|||||||
# 目标元数据(包含所有表定义)
|
# 目标元数据(包含所有表定义)
|
||||||
target_metadata = Base.metadata
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
# PostgreSQL 全局迁移锁,避免多进程并发执行 Alembic 导致竞态(重复加列/索引等)
|
||||||
|
# ID 由 crc32("aether-alembic-migration") 拼接生成,仅需全局唯一即可
|
||||||
|
MIGRATION_ADVISORY_LOCK_ID = 582694137405821
|
||||||
|
|
||||||
|
|
||||||
def run_migrations_offline() -> None:
|
def run_migrations_offline() -> None:
|
||||||
"""
|
"""
|
||||||
@@ -83,15 +89,31 @@ def run_migrations_online() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
with connectable.connect() as connection:
|
||||||
context.configure(
|
lock_acquired = False
|
||||||
connection=connection,
|
try:
|
||||||
target_metadata=target_metadata,
|
if connection.dialect.name == "postgresql":
|
||||||
compare_type=True, # 比较列类型变更
|
connection.execute(
|
||||||
compare_server_default=True, # 比较默认值变更
|
text("SELECT pg_advisory_lock(:lock_id)"),
|
||||||
)
|
{"lock_id": MIGRATION_ADVISORY_LOCK_ID},
|
||||||
|
)
|
||||||
|
lock_acquired = True
|
||||||
|
|
||||||
with context.begin_transaction():
|
context.configure(
|
||||||
context.run_migrations()
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
compare_type=True, # 比较列类型变更
|
||||||
|
compare_server_default=True, # 比较默认值变更
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
finally:
|
||||||
|
if lock_acquired:
|
||||||
|
connection.rollback()
|
||||||
|
connection.execute(
|
||||||
|
text("SELECT pg_advisory_unlock(:lock_id)"),
|
||||||
|
{"lock_id": MIGRATION_ADVISORY_LOCK_ID},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 根据模式选择运行方式
|
# 根据模式选择运行方式
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
import sqlalchemy as sa
|
from sqlalchemy import text
|
||||||
from sqlalchemy import inspect
|
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
|
||||||
@@ -24,33 +23,24 @@ depends_on: str | Sequence[str] | None = None
|
|||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
inspector = inspect(conn)
|
# Use PostgreSQL native IF NOT EXISTS to avoid duplicate-column races
|
||||||
existing_columns = {col["name"] for col in inspector.get_columns("usage")}
|
# when migrations are triggered concurrently (e.g. startup + manual run).
|
||||||
|
conn.execute(text("ALTER TABLE usage ADD COLUMN IF NOT EXISTS provider_request_body JSON"))
|
||||||
if "provider_request_body" not in existing_columns:
|
conn.execute(
|
||||||
op.add_column("usage", sa.Column("provider_request_body", sa.JSON(), nullable=True))
|
text("ALTER TABLE usage ADD COLUMN IF NOT EXISTS provider_request_body_compressed BYTEA")
|
||||||
if "provider_request_body_compressed" not in existing_columns:
|
)
|
||||||
op.add_column(
|
conn.execute(text("ALTER TABLE usage ADD COLUMN IF NOT EXISTS client_response_body JSON"))
|
||||||
"usage", sa.Column("provider_request_body_compressed", sa.LargeBinary(), nullable=True)
|
conn.execute(
|
||||||
)
|
text("ALTER TABLE usage ADD COLUMN IF NOT EXISTS client_response_body_compressed BYTEA")
|
||||||
if "client_response_body" not in existing_columns:
|
)
|
||||||
op.add_column("usage", sa.Column("client_response_body", sa.JSON(), nullable=True))
|
|
||||||
if "client_response_body_compressed" not in existing_columns:
|
|
||||||
op.add_column(
|
|
||||||
"usage", sa.Column("client_response_body_compressed", sa.LargeBinary(), nullable=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
conn = op.get_bind()
|
conn = op.get_bind()
|
||||||
inspector = inspect(conn)
|
|
||||||
existing_columns = {col["name"] for col in inspector.get_columns("usage")}
|
|
||||||
|
|
||||||
for col in (
|
for col in (
|
||||||
"client_response_body_compressed",
|
"client_response_body_compressed",
|
||||||
"client_response_body",
|
"client_response_body",
|
||||||
"provider_request_body_compressed",
|
"provider_request_body_compressed",
|
||||||
"provider_request_body",
|
"provider_request_body",
|
||||||
):
|
):
|
||||||
if col in existing_columns:
|
conn.execute(text(f"ALTER TABLE usage DROP COLUMN IF EXISTS {col}"))
|
||||||
op.drop_column("usage", col)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user