diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5d04dd..5bcc063 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -328,3 +328,48 @@ jobs: - name: Production build run: npm run build + + # ------------------------------------------------------------------------- + # ruff (lint) — added 2026-04-25 per issue #33. Gates Python lint on every + # PR and push to master. No live DB needed; --no-deps keeps it fast. + # Mirror the docker-compose-via-image pattern used by the bootstrap-failure + # job so CI lint and local lint produce the same result. + # + # NOTE: making this a REQUIRED check on master needs a separate + # branch-protection setting (Settings -> Branches -> master -> + # Require status checks -> add `ruff (lint)`). The workflow change here + # makes the job RUN; the protection setting makes it BLOCK merges. + # ------------------------------------------------------------------------- + ruff: + name: ruff (lint) + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: "Synthesize minimal .env (compose env_file: directive requires it to exist)" + # ruff check doesn't need any of these values, but docker-compose.yml's + # service-level `env_file: .env` directive requires the file to exist + # or `docker compose run` errors out before the container even starts. + # Match the per-job .env synthesis pattern used by `backend` and + # `bootstrap-failure`. Values are placeholders; ruff never reads them. + run: | + umask 077 + cat > .env < list[dict]: model = model or settings.vision_model - from PIL import Image import io try: import pdfplumber diff --git a/backend/app/ingestion/scheduler.py b/backend/app/ingestion/scheduler.py index 70524d6..e7169c4 100644 --- a/backend/app/ingestion/scheduler.py +++ b/backend/app/ingestion/scheduler.py @@ -87,7 +87,6 @@ def cleanup_audit_logs(): """Archive and then delete audit log entries older than the configured retention period.""" import asyncio import json - import os from datetime import datetime, timezone, timedelta from pathlib import Path from sqlalchemy import delete, select diff --git a/backend/app/llm/context_manager.py b/backend/app/llm/context_manager.py index 8ee83ba..d8e04c3 100644 --- a/backend/app/llm/context_manager.py +++ b/backend/app/llm/context_manager.py @@ -10,7 +10,7 @@ import logging import re -from dataclasses import dataclass, field +from dataclasses import dataclass logger = logging.getLogger(__name__) diff --git a/backend/app/main.py b/backend/app/main.py index 1649a3e..0d6c023 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -8,6 +8,9 @@ from app.audit import AuditMiddleware, audit_router from app.auth import auth_router, register_router, users_router from app.config import APP_VERSION, settings +from app.database import engine +from app.models.user import User, UserRole +from app.schemas.user import AdminUserCreate class PortalModeResponse(BaseModel): @@ -28,9 +31,6 @@ class PortalModeResponse(BaseModel): "self-registration." ), ) -from app.database import engine -from app.models.user import User, UserRole -from app.schemas.user import AdminUserCreate @asynccontextmanager diff --git a/backend/app/models/audit.py b/backend/app/models/audit.py index dc4cef4..849beee 100644 --- a/backend/app/models/audit.py +++ b/backend/app/models/audit.py @@ -1,7 +1,7 @@ import uuid from datetime import datetime -from sqlalchemy import DateTime, Index, String, Text, Boolean, func +from sqlalchemy import DateTime, Index, String, Boolean, func from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column diff --git a/backend/app/models/connectors.py b/backend/app/models/connectors.py index aad2d53..6dcbfdc 100644 --- a/backend/app/models/connectors.py +++ b/backend/app/models/connectors.py @@ -1,4 +1,3 @@ -import uuid from datetime import datetime from sqlalchemy import DateTime, String, Text, Integer, func from sqlalchemy.dialects.postgresql import JSONB diff --git a/backend/app/models/document.py b/backend/app/models/document.py index 434c2f7..f98887d 100644 --- a/backend/app/models/document.py +++ b/backend/app/models/document.py @@ -5,7 +5,7 @@ from pgvector.sqlalchemy import Vector from sqlalchemy import ( - Boolean, DateTime, Enum, Float, ForeignKey, Index, Integer, + Boolean, DateTime, Enum, ForeignKey, Index, Integer, String, Text, TypeDecorator, func, ) from sqlalchemy.dialects.postgresql import JSONB, UUID diff --git a/backend/app/models/exemption.py b/backend/app/models/exemption.py index 14c80fe..bd5395a 100644 --- a/backend/app/models/exemption.py +++ b/backend/app/models/exemption.py @@ -3,7 +3,7 @@ from datetime import datetime from sqlalchemy import Boolean, DateTime, Enum, Float, ForeignKey, Integer, String, Text, func -from sqlalchemy.dialects.postgresql import JSONB, UUID +from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column from app.models.user import Base diff --git a/backend/app/models/search.py b/backend/app/models/search.py index 630b6df..5dd9a71 100644 --- a/backend/app/models/search.py +++ b/backend/app/models/search.py @@ -1,7 +1,7 @@ import uuid from datetime import datetime -from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, func +from sqlalchemy import DateTime, Float, ForeignKey, Integer, Text, func from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column diff --git a/backend/app/models/sync_failure.py b/backend/app/models/sync_failure.py index 10d7f27..b4712f7 100644 --- a/backend/app/models/sync_failure.py +++ b/backend/app/models/sync_failure.py @@ -4,7 +4,7 @@ from datetime import datetime from sqlalchemy import ( - Boolean, DateTime, ForeignKey, Index, Integer, String, Text, func, + DateTime, ForeignKey, Index, Integer, String, Text, func, ) from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column diff --git a/backend/app/schemas/document.py b/backend/app/schemas/document.py index 281b264..5e8a7fa 100644 --- a/backend/app/schemas/document.py +++ b/backend/app/schemas/document.py @@ -1,6 +1,6 @@ import uuid from datetime import datetime -from pydantic import BaseModel, field_validator, model_validator +from pydantic import BaseModel, field_validator from app.models.document import IngestionStatus, SourceType diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 49514dc..2d3159b 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -2,7 +2,7 @@ from datetime import datetime from fastapi_users import schemas -from pydantic import BaseModel, model_validator +from pydantic import model_validator from app.models.user import UserRole diff --git a/backend/app/search/router.py b/backend/app/search/router.py index dc8de5c..baf1286 100644 --- a/backend/app/search/router.py +++ b/backend/app/search/router.py @@ -8,7 +8,7 @@ from app.auth.dependencies import require_role, require_department_filter from app.database import get_async_session from app.models.departments import Department -from app.models.document import DataSource, Document, DocumentChunk +from app.models.document import DataSource, Document from app.models.search import SearchQuery, SearchResult, SearchSession from app.models.user import User, UserRole from app.schemas.search import ( diff --git a/backend/app/service_accounts/router.py b/backend/app/service_accounts/router.py index 65bcc60..e450231 100644 --- a/backend/app/service_accounts/router.py +++ b/backend/app/service_accounts/router.py @@ -2,7 +2,7 @@ import uuid from hashlib import sha256 -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession diff --git a/backend/scripts/generate_pdf.py b/backend/scripts/generate_pdf.py index 19b6ced..a9fe29b 100644 --- a/backend/scripts/generate_pdf.py +++ b/backend/scripts/generate_pdf.py @@ -4,19 +4,16 @@ """ import os -import sys from reportlab.lib.pagesizes import letter from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.units import inch from reportlab.lib import colors -from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY +from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY from reportlab.platypus import ( SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak, HRFlowable, KeepTogether ) from reportlab.platypus.flowables import Flowable -from reportlab.graphics.shapes import Drawing, Rect, String, Line, Group -from reportlab.graphics import renderPDF # ── Output path ────────────────────────────────────────────────────────────── SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -218,7 +215,6 @@ def draw(self): fill=BLUE, text_color=WHITE) # Horizontal connectors from FastAPI to side services - cx = api_x + (bw + 10) / 2 # Left services: PostgreSQL, pgvector pg_x = 30 diff --git a/backend/scripts/seed_rules.py b/backend/scripts/seed_rules.py index 68ffed4..599d4eb 100644 --- a/backend/scripts/seed_rules.py +++ b/backend/scripts/seed_rules.py @@ -1,6 +1,5 @@ """Seed exemption rules for all 50 states + DC.""" import asyncio -import uuid from app.database import async_session_maker from app.models.exemption import ExemptionRule, RuleType from app.models.user import User diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index c0e1eaf..3a57051 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -32,7 +32,7 @@ from app.config import settings from app.database import get_async_session from app.main import create_app -from app.models.user import Base, User, UserRole +from app.models.user import User, UserRole from app.models.departments import Department from app.models.sync_failure import SyncFailure, SyncRunLog # noqa: F401 — registers with Base.metadata diff --git a/backend/tests/test_at_rest_encryption.py b/backend/tests/test_at_rest_encryption.py index e3479dd..f3d9647 100644 --- a/backend/tests/test_at_rest_encryption.py +++ b/backend/tests/test_at_rest_encryption.py @@ -25,7 +25,6 @@ from sqlalchemy import text from app.config import Settings -from app.models.document import SourceType from app.security.at_rest import ( AtRestDecryptionError, decrypt_json, diff --git a/backend/tests/test_audit.py b/backend/tests/test_audit.py index 3ef9b3c..dc60c49 100644 --- a/backend/tests/test_audit.py +++ b/backend/tests/test_audit.py @@ -57,7 +57,7 @@ async def test_write_audit_log_direct(client: AsyncClient): """Verify audit log entries can be written and read back.""" from tests.conftest import test_session_maker from app.audit.logger import write_audit_log - from sqlalchemy import select, func + from sqlalchemy import select from app.models.audit import AuditLog async with test_session_maker() as session: diff --git a/backend/tests/test_base_connector.py b/backend/tests/test_base_connector.py index 4cf3caa..1e45eeb 100644 --- a/backend/tests/test_base_connector.py +++ b/backend/tests/test_base_connector.py @@ -1,4 +1,5 @@ import pytest +from app.connectors import get_connector from app.connectors.base import BaseConnector, DiscoveredRecord, FetchedDocument, HealthCheckResult, HealthStatus @@ -26,9 +27,6 @@ def test_base_connector_close_is_noop(): c.close() # must not raise AttributeError or any other error -from app.connectors import get_connector - - def test_factory_rest_api(): connector = get_connector("rest_api", { "base_url": "https://example.gov", diff --git a/backend/tests/test_bootstrap_integration.py b/backend/tests/test_bootstrap_integration.py index 98d778a..0ef30f3 100644 --- a/backend/tests/test_bootstrap_integration.py +++ b/backend/tests/test_bootstrap_integration.py @@ -62,7 +62,7 @@ # Generate a real Fernet key once per test module so every _minimal_env() # call gets a valid key by default; tests that specifically want to # verify the encryption-key validator can override it. -from cryptography.fernet import Fernet as _Fernet +from cryptography.fernet import Fernet as _Fernet # noqa: E402 late import — comment block above documents T6/ENG-001 test override pattern requiring this be tied to module-level constants _VALID_ENCRYPTION_KEY = _Fernet.generate_key().decode() diff --git a/backend/tests/test_circuit_breaker.py b/backend/tests/test_circuit_breaker.py index d720a77..9d92a95 100644 --- a/backend/tests/test_circuit_breaker.py +++ b/backend/tests/test_circuit_breaker.py @@ -1,7 +1,7 @@ # backend/tests/test_circuit_breaker.py """P7 circuit breaker tests — real end-to-end calls asserting DB state.""" import uuid -from datetime import datetime, timezone +from datetime import timezone from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -165,7 +165,7 @@ async def test_success_resets_consecutive_failure_count(db_session): @pytest.mark.asyncio async def test_zero_records_discovered_does_not_increment_counter(db_session): """discover() returns 0 five times → counter=0, no circuit open (M8).""" - from unittest.mock import AsyncMock, patch + from unittest.mock import AsyncMock import uuid source_id = uuid.uuid4() diff --git a/backend/tests/test_civiccore_migration_gates.py b/backend/tests/test_civiccore_migration_gates.py index c604a9f..32f1f20 100644 --- a/backend/tests/test_civiccore_migration_gates.py +++ b/backend/tests/test_civiccore_migration_gates.py @@ -44,7 +44,6 @@ import os import subprocess -import sys import uuid from collections.abc import Iterator from pathlib import Path diff --git a/backend/tests/test_datasources.py b/backend/tests/test_datasources.py index a115c39..2d6f838 100644 --- a/backend/tests/test_datasources.py +++ b/backend/tests/test_datasources.py @@ -3,7 +3,7 @@ from httpx import AsyncClient from sqlalchemy import text -from app.models.document import DataSource, SourceType +from app.models.document import SourceType from tests.conftest import build_data_source @pytest.mark.asyncio diff --git a/backend/tests/test_exemptions.py b/backend/tests/test_exemptions.py index 0d26942..d2fcd89 100644 --- a/backend/tests/test_exemptions.py +++ b/backend/tests/test_exemptions.py @@ -1,4 +1,3 @@ -import uuid import pytest from httpx import AsyncClient from app.exemptions.engine import scan_chunk_builtin, scan_text_with_regex, scan_text_with_keywords diff --git a/backend/tests/test_imap_connector.py b/backend/tests/test_imap_connector.py index 83fafb4..f207577 100644 --- a/backend/tests/test_imap_connector.py +++ b/backend/tests/test_imap_connector.py @@ -9,8 +9,6 @@ from app.connectors.imap_email import ( ImapEmailConnector, is_attachment_safe, - ALLOWED_MIME_TYPES, - BLOCKED_EXTENSIONS, MAX_ATTACHMENT_BYTES, ) diff --git a/backend/tests/test_info_leak_hardening.py b/backend/tests/test_info_leak_hardening.py index 2603976..a72cea0 100644 --- a/backend/tests/test_info_leak_hardening.py +++ b/backend/tests/test_info_leak_hardening.py @@ -73,7 +73,7 @@ async def _seed_exemption_flag_on_request( doc = Document( source_id=source.id, source_path=f"/seed/flag-doc-{uuid.uuid4().hex[:6]}.pdf", - filename=f"flag-doc.pdf", + filename="flag-doc.pdf", file_type="pdf", file_hash=uuid.uuid4().hex, file_size=128, diff --git a/backend/tests/test_ingestion_retry.py b/backend/tests/test_ingestion_retry.py index 522841e..e4d6544 100644 --- a/backend/tests/test_ingestion_retry.py +++ b/backend/tests/test_ingestion_retry.py @@ -9,7 +9,8 @@ async def test_re_ingest_requires_failed_status(client: AsyncClient, admin_token """POST /datasources/documents/{id}/re-ingest rejects non-failed docs.""" import uuid # Upload a file to create a document (will be in pending/completed state) - import tempfile, os + import tempfile + import os with tempfile.NamedTemporaryFile(suffix=".txt", delete=False, mode="w") as f: f.write("Test content for retry test") tmp_path = f.name diff --git a/backend/tests/test_ingestion_tasks.py b/backend/tests/test_ingestion_tasks.py index a5cc8ce..00b8271 100644 --- a/backend/tests/test_ingestion_tasks.py +++ b/backend/tests/test_ingestion_tasks.py @@ -133,7 +133,6 @@ async def test_close_called_on_discover_failure(db_session): @pytest.mark.asyncio async def test_cursor_written_on_full_success(db_session): """last_sync_cursor and last_sync_at are set after a clean run.""" - from app.models.document import DataSource source_id = uuid.uuid4() await _seed_source(db_session, source_id, "cursor-success") diff --git a/backend/tests/test_manual_drop.py b/backend/tests/test_manual_drop.py index 474a7a0..945135d 100644 --- a/backend/tests/test_manual_drop.py +++ b/backend/tests/test_manual_drop.py @@ -6,7 +6,6 @@ from app.connectors.manual_drop import ( ManualDropConnector, - ACCEPTED_EXTENSIONS, MAX_FILE_BYTES, ) @@ -248,7 +247,7 @@ async def test_non_recursive_skips_subdirs(drop_dir): # ── Pipeline Dispatch Tests ─────────────────────────────────────────────────── -from app.connectors import manual_drop as _drop_mod +from app.connectors import manual_drop as _drop_mod # noqa: E402 section-local import for "Pipeline Dispatch Tests" group below @pytest.mark.asyncio diff --git a/backend/tests/test_migration_014.py b/backend/tests/test_migration_014.py index fad1172..bd45c04 100644 --- a/backend/tests/test_migration_014.py +++ b/backend/tests/test_migration_014.py @@ -5,7 +5,6 @@ inspecting the model and checking that the upgrade/downgrade functions contain the expected operations. Run against the integration test DB after applying the migration. """ -import pytest def test_migration_014_revision_metadata(): @@ -41,7 +40,7 @@ def test_updated_at_column_on_document_model(): def test_migration_014_has_partial_index_for_structured(): """Migration 014 upgrade() creates the structured partial UNIQUE index.""" - import ast, pathlib + import pathlib src = pathlib.Path("alembic/versions/014_p6a_idempotency.py").read_text() assert "uq_documents_structured_path" in src assert "connector_type IN" in src diff --git a/backend/tests/test_migration_015.py b/backend/tests/test_migration_015.py index 4983f41..bf72eb7 100644 --- a/backend/tests/test_migration_015.py +++ b/backend/tests/test_migration_015.py @@ -2,7 +2,7 @@ import pytest from sqlalchemy import text -from app.models.document import DataSource, SourceType +from app.models.document import SourceType from tests.conftest import build_data_source diff --git a/backend/tests/test_notification_dispatch.py b/backend/tests/test_notification_dispatch.py index cdc345b..33945c3 100644 --- a/backend/tests/test_notification_dispatch.py +++ b/backend/tests/test_notification_dispatch.py @@ -140,7 +140,7 @@ async def test_all_templates_render_with_router_context_keys(client, admin_token ) assert render_failures == [], ( - f"Templates reference variables not provided by the router:\n" + "Templates reference variables not provided by the router:\n" + "\n".join(render_failures) ) diff --git a/backend/tests/test_parsers.py b/backend/tests/test_parsers.py index 45ce884..41780b3 100644 --- a/backend/tests/test_parsers.py +++ b/backend/tests/test_parsers.py @@ -1,7 +1,6 @@ import tempfile import zipfile from pathlib import Path -import pytest from app.ingestion.parsers import detect_parser, is_image_file from app.ingestion.parsers.text import TextParser from app.ingestion.parsers.csv_parser import CsvParser diff --git a/backend/tests/test_pipeline.py b/backend/tests/test_pipeline.py index 0a39abd..460001e 100644 --- a/backend/tests/test_pipeline.py +++ b/backend/tests/test_pipeline.py @@ -28,7 +28,7 @@ async def test_ingest_file_txt(client, admin_token): """Integration test: ingest a text file through the full pipeline.""" from tests.conftest import test_session_maker from app.ingestion.pipeline import ingest_file - from app.models.document import DataSource, Document, IngestionStatus, SourceType + from app.models.document import DataSource, IngestionStatus, SourceType from app.models.user import User from sqlalchemy import select diff --git a/backend/tests/test_pipeline_idempotency.py b/backend/tests/test_pipeline_idempotency.py index a6a1b01..b862b1b 100644 --- a/backend/tests/test_pipeline_idempotency.py +++ b/backend/tests/test_pipeline_idempotency.py @@ -3,9 +3,8 @@ import json import uuid import pytest -from sqlalchemy.ext.asyncio import AsyncSession -from app.models.document import DataSource, SourceType +from app.models.document import SourceType from tests.conftest import build_data_source @@ -328,7 +327,9 @@ async def worker(session): @pytest.mark.asyncio async def test_concurrent_binary_insert_race(self, db_session, db_session_factory): """Two workers insert same (source_id, file_hash) simultaneously → 1 document row.""" - import asyncio, os, tempfile + import asyncio + import os + import tempfile from pathlib import Path from unittest.mock import patch, AsyncMock from sqlalchemy.exc import IntegrityError diff --git a/backend/tests/test_rest_connector.py b/backend/tests/test_rest_connector.py index 34c3821..0ddf742 100644 --- a/backend/tests/test_rest_connector.py +++ b/backend/tests/test_rest_connector.py @@ -494,7 +494,7 @@ def test_source_path_record_id_encoded(self): # P7 adversarial — Retry-After header edge cases # --------------------------------------------------------------------------- -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, patch # noqa: E402 section-local import for "P7 adversarial — Retry-After header edge cases" test class below class TestRetryAfterAdversarial: diff --git a/backend/tests/test_retry.py b/backend/tests/test_retry.py index 8aa8e71..cf13f32 100644 --- a/backend/tests/test_retry.py +++ b/backend/tests/test_retry.py @@ -1,4 +1,3 @@ -import asyncio import pytest import httpx from unittest.mock import AsyncMock, patch diff --git a/backend/tests/test_scheduler.py b/backend/tests/test_scheduler.py index e5116a8..4f4ce5a 100644 --- a/backend/tests/test_scheduler.py +++ b/backend/tests/test_scheduler.py @@ -1,13 +1,13 @@ # backend/tests/test_scheduler.py """P6b scheduler tests — TDD order. Tests must fail before implementation.""" import uuid -from datetime import datetime, timezone, timedelta -from unittest.mock import AsyncMock, MagicMock, patch +from datetime import datetime, timezone +from unittest.mock import patch import pytest from croniter import croniter -from app.models.document import DataSource, SourceType +from app.models.document import SourceType from tests.conftest import build_data_source diff --git a/backend/tests/test_search_engine.py b/backend/tests/test_search_engine.py index efe51b7..12eb6df 100644 --- a/backend/tests/test_search_engine.py +++ b/backend/tests/test_search_engine.py @@ -1,5 +1,4 @@ import uuid -import pytest from app.search.engine import reciprocal_rank_fusion, SearchHit diff --git a/backend/tests/test_smtp_delivery.py b/backend/tests/test_smtp_delivery.py index a50cd64..aa1ee9f 100644 --- a/backend/tests/test_smtp_delivery.py +++ b/backend/tests/test_smtp_delivery.py @@ -1,8 +1,6 @@ """Tests for SMTP notification delivery.""" -import uuid from unittest.mock import patch, MagicMock -from datetime import datetime, timezone import pytest from httpx import AsyncClient diff --git a/backend/tests/test_sync_failures.py b/backend/tests/test_sync_failures.py index e6850a1..19954ed 100644 --- a/backend/tests/test_sync_failures.py +++ b/backend/tests/test_sync_failures.py @@ -6,7 +6,7 @@ import pytest from sqlalchemy import text -from app.models.document import DataSource, SourceType +from app.models.document import SourceType from tests.conftest import build_data_source diff --git a/backend/tests/test_sync_failures_router.py b/backend/tests/test_sync_failures_router.py index 6768c50..81d687c 100644 --- a/backend/tests/test_sync_failures_router.py +++ b/backend/tests/test_sync_failures_router.py @@ -102,7 +102,6 @@ async def test_list_sync_failures_requires_auth(client: AsyncClient): @pytest.mark.asyncio async def test_unpause_source(client: AsyncClient, admin_token: str, db_session): """POST /datasources/{id}/unpause resets paused state.""" - from app.models.document import DataSource source_id = uuid.uuid4() await _seed_source( diff --git a/backend/tests/test_sync_runner_cursor.py b/backend/tests/test_sync_runner_cursor.py index e8ee4be..3db32c9 100644 --- a/backend/tests/test_sync_runner_cursor.py +++ b/backend/tests/test_sync_runner_cursor.py @@ -2,7 +2,6 @@ """P7 partial-failure cursor advance tests — real end-to-end calls asserting DB state.""" import uuid from unittest.mock import AsyncMock, MagicMock, patch -from datetime import datetime, timezone import pytest from sqlalchemy import text diff --git a/backend/tests/test_sync_runner_pipeline_failures.py b/backend/tests/test_sync_runner_pipeline_failures.py index f91187a..b506db5 100644 --- a/backend/tests/test_sync_runner_pipeline_failures.py +++ b/backend/tests/test_sync_runner_pipeline_failures.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from sqlalchemy import text -from app.models.document import DataSource, SourceType +from app.models.document import SourceType from tests.conftest import build_data_source diff --git a/backend/tests/test_sync_runner_retry_cap.py b/backend/tests/test_sync_runner_retry_cap.py index b72c798..b74bff7 100644 --- a/backend/tests/test_sync_runner_retry_cap.py +++ b/backend/tests/test_sync_runner_retry_cap.py @@ -5,7 +5,7 @@ import pytest from sqlalchemy import text -from app.models.document import DataSource, SourceType +from app.models.document import SourceType from tests.conftest import build_data_source diff --git a/backend/tests/test_sync_runner_retry_layers.py b/backend/tests/test_sync_runner_retry_layers.py index 92d4d9a..05a1fc2 100644 --- a/backend/tests/test_sync_runner_retry_layers.py +++ b/backend/tests/test_sync_runner_retry_layers.py @@ -5,7 +5,7 @@ import pytest from sqlalchemy import text -from app.models.document import DataSource, SourceType +from app.models.document import SourceType from tests.conftest import build_data_source @@ -84,7 +84,6 @@ async def mock_discover(): async def test_retrying_row_resolved_on_success(db_session): """A retrying row that fetches successfully → status=resolved, resolved_at set.""" from app.models.sync_failure import SyncFailure - from sqlalchemy import select source_id = uuid.uuid4() await _seed_source(db_session, source_id, "resolve-test") diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh index 463e102..08e37c7 100644 --- a/scripts/verify-release.sh +++ b/scripts/verify-release.sh @@ -96,6 +96,31 @@ for f in README.md CHANGELOG.md CONTRIBUTING.md LICENSE .gitignore docs/index.ht fi done +# --- 4. ruff lint ------------------------------------------------------------ +# Host-side ruff (operators: `pip install --user ruff`). Falls back to +# `python -m ruff` if the binary isn't on PATH. Container ruff would scan +# image-baked source (potentially stale relative to current working tree), +# which would give false positives/negatives; host ruff scans on-disk files. +# CI uses container ruff via .github/workflows/ci.yml because CI always +# builds a fresh api image first. +info "4. ruff lint" +if command -v ruff >/dev/null 2>&1; then + RUFF_CMD="ruff" +elif python -m ruff --version >/dev/null 2>&1; then + RUFF_CMD="python -m ruff" +else + RUFF_CMD="" + fail "ruff: not installed locally — install with: pip install --user ruff" +fi + +if [ -n "$RUFF_CMD" ]; then + if (cd backend && $RUFF_CMD check .) > /tmp/ruff-verify-release.out 2>&1; then + pass "ruff: 0 violations" + else + fail "ruff: violations present (see /tmp/ruff-verify-release.out for details)" + fi +fi + # --- summary ----------------------------------------------------------------- echo "" if [ "$FAILED" -eq 0 ]; then