fix(deploy): fix render-worker build context + migration 040 idempotency

- docker-compose.yml: change render-worker build context from ./render-worker
  to . (project root) so pyproject.toml is accessible; update dockerfile path
- render-worker/Dockerfile: update COPY paths for new build context;
  install Python 3.11 via deadsnakes PPA (Ubuntu 22.04 ships 3.10 which
  fails the >=3.11 requirement in pyproject.toml)
- 040_media_assets.py: rewrite upgrade() with raw idempotent SQL (CREATE TYPE
  inside DO $$ EXCEPTION WHEN duplicate_object $$; CREATE TABLE IF NOT EXISTS;
  CREATE INDEX IF NOT EXISTS) to handle pre-existing enum from partial runs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 17:32:42 +01:00
parent bf0c55c970
commit 7706c514c8
3 changed files with 69 additions and 54 deletions
+48 -46
View File
@@ -14,56 +14,58 @@ depends_on = None
def upgrade(): def upgrade():
# Create enum type # Use raw SQL throughout for full idempotency (enum may exist from a partial run)
op.execute( op.execute("""
"CREATE TYPE media_asset_type AS ENUM (" DO $$ BEGIN
"'thumbnail','still','turntable','stl_low','stl_high'," CREATE TYPE media_asset_type AS ENUM (
"'gltf_geometry','gltf_production','blend_production')" 'thumbnail','still','turntable','stl_low','stl_high',
) 'gltf_geometry','gltf_production','blend_production'
);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
""")
op.create_table( op.execute("""
'media_assets', CREATE TABLE IF NOT EXISTS media_assets (
sa.Column('id', UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()')), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sa.Column('tenant_id', UUID(as_uuid=True), sa.ForeignKey('tenants.id', ondelete='CASCADE'), nullable=True), tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
sa.Column('product_id', UUID(as_uuid=True), sa.ForeignKey('products.id', ondelete='CASCADE'), nullable=True), product_id UUID REFERENCES products(id) ON DELETE CASCADE,
sa.Column('cad_file_id', UUID(as_uuid=True), sa.ForeignKey('cad_files.id', ondelete='SET NULL'), nullable=True), cad_file_id UUID REFERENCES cad_files(id) ON DELETE SET NULL,
sa.Column('order_line_id', UUID(as_uuid=True), sa.ForeignKey('order_lines.id', ondelete='SET NULL'), nullable=True), order_line_id UUID REFERENCES order_lines(id) ON DELETE SET NULL,
sa.Column('workflow_run_id', UUID(as_uuid=True), sa.ForeignKey('workflow_runs.id', ondelete='SET NULL'), nullable=True), workflow_run_id UUID REFERENCES workflow_runs(id) ON DELETE SET NULL,
sa.Column( asset_type media_asset_type NOT NULL,
'asset_type', storage_key TEXT NOT NULL,
sa.Enum( file_size_bytes BIGINT,
'thumbnail', 'still', 'turntable', mime_type VARCHAR(100),
'stl_low', 'stl_high', width INTEGER,
'gltf_geometry', 'gltf_production', 'blend_production', height INTEGER,
name='media_asset_type', duration_s FLOAT,
), render_config JSONB,
nullable=False, is_archived BOOLEAN NOT NULL DEFAULT false,
), created_at TIMESTAMP NOT NULL DEFAULT NOW()
sa.Column('storage_key', sa.Text, nullable=False),
sa.Column('file_size_bytes', sa.BigInteger, nullable=True),
sa.Column('mime_type', sa.String(100), nullable=True),
sa.Column('width', sa.Integer, nullable=True),
sa.Column('height', sa.Integer, nullable=True),
sa.Column('duration_s', sa.Float, nullable=True),
sa.Column('render_config', JSONB, nullable=True),
sa.Column('is_archived', sa.Boolean, nullable=False, server_default='false'),
sa.Column('created_at', sa.DateTime, nullable=False, server_default=sa.text('NOW()')),
) )
op.create_index('ix_media_assets_product', 'media_assets', ['product_id']) """)
op.create_index('ix_media_assets_tenant', 'media_assets', ['tenant_id'])
op.create_index('ix_media_assets_order_line', 'media_assets', ['order_line_id']) op.execute("CREATE INDEX IF NOT EXISTS ix_media_assets_product ON media_assets (product_id)")
op.create_index('ix_media_assets_asset_type', 'media_assets', ['asset_type']) op.execute("CREATE INDEX IF NOT EXISTS ix_media_assets_tenant ON media_assets (tenant_id)")
op.execute("CREATE INDEX IF NOT EXISTS ix_media_assets_order_line ON media_assets (order_line_id)")
op.execute("CREATE INDEX IF NOT EXISTS ix_media_assets_asset_type ON media_assets (asset_type)")
# RLS
op.execute("ALTER TABLE media_assets ENABLE ROW LEVEL SECURITY") op.execute("ALTER TABLE media_assets ENABLE ROW LEVEL SECURITY")
op.execute( op.execute("""
"""CREATE POLICY tenant_isolation ON media_assets DO $$ BEGIN
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid)""" CREATE POLICY tenant_isolation ON media_assets
) USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
op.execute( EXCEPTION WHEN duplicate_object THEN NULL;
"""CREATE POLICY admin_bypass ON media_assets END $$;
USING (current_setting('app.current_tenant_id', true) = 'bypass')""" """)
) op.execute("""
DO $$ BEGIN
CREATE POLICY admin_bypass ON media_assets
USING (current_setting('app.current_tenant_id', true) = 'bypass');
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
""")
def downgrade(): def downgrade():
+2 -2
View File
@@ -114,8 +114,8 @@ services:
render-worker: render-worker:
build: build:
context: ./render-worker context: .
dockerfile: Dockerfile dockerfile: render-worker/Dockerfile
args: args:
- BLENDER_VERSION=${BLENDER_VERSION:-5.0.1} - BLENDER_VERSION=${BLENDER_VERSION:-5.0.1}
environment: environment:
+18 -5
View File
@@ -11,9 +11,17 @@ ENV PYOPENGL_PLATFORM=osmesa
ENV VTK_DEFAULT_EGL=0 ENV VTK_DEFAULT_EGL=0
# Runtime libraries for cadquery/OCC + Blender 5.x headless # Runtime libraries for cadquery/OCC + Blender 5.x headless
# Also installs Python 3.11 via deadsnakes PPA (Ubuntu 22.04 ships 3.10)
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common gnupg gpg-agent \
&& add-apt-repository ppa:deadsnakes/ppa -y \
&& apt-get update && apt-get install -y --no-install-recommends \
python3.11 \
python3.11-dev \
python3.11-distutils \
python3-pip \ python3-pip \
python3-dev \ python3-dev \
curl \
libpq-dev \ libpq-dev \
gcc \ gcc \
libxrender1 \ libxrender1 \
@@ -40,21 +48,26 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
WORKDIR /app WORKDIR /app
# Use Python 3.11 as the default pip target
RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11 \
&& update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 \
&& update-alternatives --install /usr/bin/pip3 pip3 /usr/local/bin/pip3.11 1
# Install backend Python dependencies (includes celery, sqlalchemy, fastapi, etc.) # Install backend Python dependencies (includes celery, sqlalchemy, fastapi, etc.)
COPY pyproject.toml . COPY backend/pyproject.toml .
RUN pip3 install --no-cache-dir -e . RUN pip3 install --no-cache-dir -e .
# Install cadquery (heavy — installed after backend deps for better layer caching) # Install cadquery (heavy — installed after backend deps for better layer caching)
RUN pip3 install --no-cache-dir "cadquery>=2.4.0" RUN pip3 install --no-cache-dir "cadquery>=2.4.0"
# Copy render scripts # Copy render scripts
COPY scripts/ /render-scripts/ COPY render-worker/scripts/ /render-scripts/
# Version check script — fails fast if Blender < 5.0.1 # Version check script — fails fast if Blender < 5.0.1
COPY check_version.py /check_version.py COPY render-worker/check_version.py /check_version.py
# Copy app code (overridden by volume mount in docker-compose) # Copy backend app code (overridden by volume mount in docker-compose)
COPY . . COPY backend/ .
# Verify Blender version at build time if binary is available # Verify Blender version at build time if binary is available
# (skipped during build since /opt/blender is a host mount) # (skipped during build since /opt/blender is a host mount)