Skip to content

Commit 281c9bb

Browse files
committed
Add GitHub Actions CI workflows for backend and frontend
Add mypy and tsc type checking to pre-commit hooks Add Codecov upload to CI workflows with backend/frontend flags Fix prettier version mismatch: use local npm run format hook instead of mirrors-prettier Fix Prefect ephemeral server timeout in CI Use prefect_test_harness in conftest so the server starts with in-memory SQLite (avoiding slow file-based DB migration on first run). Also raise PREFECT_SERVER_EPHEMERAL_STARTUP_TIMEOUT_SECONDS to 120s in both the test session and the CI workflow env as a safety net.
1 parent b0a6b74 commit 281c9bb

20 files changed

Lines changed: 277 additions & 155 deletions

File tree

.github/workflows/backend.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Backend CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'backend/**'
8+
- '.github/workflows/backend.yml'
9+
pull_request:
10+
branches: [main]
11+
paths:
12+
- 'backend/**'
13+
- '.github/workflows/backend.yml'
14+
workflow_dispatch:
15+
16+
jobs:
17+
test:
18+
runs-on: ubuntu-latest
19+
defaults:
20+
run:
21+
working-directory: backend
22+
env:
23+
PREFECT_SERVER_EPHEMERAL_STARTUP_TIMEOUT_SECONDS: "120"
24+
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- uses: astral-sh/setup-uv@v5
29+
with:
30+
enable-caching: true
31+
python-version: '3.12'
32+
33+
- name: Install dependencies
34+
run: uv sync --all-groups
35+
36+
- name: Lint (ruff)
37+
run: uv run ruff check .
38+
39+
- name: Format check (ruff)
40+
run: uv run ruff format --check .
41+
42+
- name: Type check (ty)
43+
run: uv run ty check app
44+
45+
- name: Test with coverage
46+
run: uv run pytest
47+
48+
- name: Upload coverage to Codecov
49+
uses: codecov/codecov-action@v5
50+
with:
51+
token: ${{ secrets.CODECOV_TOKEN }}
52+
files: backend/coverage.lcov
53+
flags: backend
54+
fail_ci_if_error: true
55+
56+
- name: Upload coverage artifact
57+
uses: actions/upload-artifact@v4
58+
if: always()
59+
with:
60+
name: backend-coverage
61+
path: backend/coverage.lcov

.github/workflows/frontend.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Frontend CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'frontend/**'
8+
- '.github/workflows/frontend.yml'
9+
pull_request:
10+
branches: [main]
11+
paths:
12+
- 'frontend/**'
13+
- '.github/workflows/frontend.yml'
14+
workflow_dispatch:
15+
16+
jobs:
17+
test:
18+
runs-on: ubuntu-latest
19+
defaults:
20+
run:
21+
working-directory: frontend
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- uses: actions/setup-node@v4
27+
with:
28+
node-version: '22'
29+
30+
- name: Install dependencies
31+
run: npm install
32+
33+
- name: Type check and build
34+
run: npm run build
35+
36+
- name: Lint
37+
run: npm run lint
38+
39+
- name: Format check
40+
run: npm run format:check
41+
42+
- name: Test with coverage
43+
run: npm run test:coverage
44+
45+
- name: Upload coverage to Codecov
46+
uses: codecov/codecov-action@v5
47+
with:
48+
token: ${{ secrets.CODECOV_TOKEN }}
49+
files: frontend/coverage/lcov.info
50+
flags: frontend
51+
fail_ci_if_error: true
52+
53+
- name: Upload coverage artifact
54+
uses: actions/upload-artifact@v4
55+
if: always()
56+
with:
57+
name: frontend-coverage
58+
path: frontend/coverage/

.pre-commit-config.yaml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,32 @@ repos:
88
- id: ruff-format
99
files: ^backend/
1010

11-
- repo: https://github.com/pre-commit/mirrors-prettier
12-
rev: v3.1.0
11+
- repo: local
1312
hooks:
1413
- id: prettier
14+
name: prettier
15+
entry: bash -c 'cd frontend && npm run format'
16+
language: system
1517
files: ^frontend/src/.*\.(ts|tsx)$
18+
pass_filenames: false
19+
20+
- id: ty
21+
name: ty
22+
entry: bash -c 'cd backend && uv run ty check app'
23+
language: system
24+
files: ^backend/.*\.py$
25+
pass_filenames: false
1626

17-
- repo: local
18-
hooks:
1927
- id: eslint
2028
name: eslint
2129
entry: bash -c 'cd frontend && npm run lint'
2230
language: system
2331
files: ^frontend/src/.*\.(ts|tsx)$
2432
pass_filenames: false
33+
34+
- id: tsc
35+
name: tsc
36+
entry: bash -c 'cd frontend && npm run build'
37+
language: system
38+
files: ^frontend/.*\.(ts|tsx)$
39+
pass_filenames: false

backend/app/flows/harvest.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ async def _resolve_grantee(session, grantee_type: str, name: str) -> uuid.UUID |
3939
if grantee_type == "user":
4040
from app.models.user import User
4141

42-
result = await session.execute(select(User.id).where(User.email == name))
42+
result = await session.execute(
43+
select(User.id).where(User.email == name) # ty: ignore[no-matching-overload]
44+
)
4345
elif grantee_type == "group":
4446
from app.models.group import Group
4547

@@ -332,10 +334,11 @@ async def harvest_instrument_flow(instrument_id: str, schedule_id: str) -> dict:
332334
from prefect.client.orchestration import get_client
333335

334336
ctx = prefect.context.get_run_context()
335-
if ctx and ctx.flow_run:
337+
flow_run = getattr(ctx, "flow_run", None)
338+
if flow_run:
336339
async with get_client() as client:
337340
await client.update_flow_run(
338-
ctx.flow_run.id,
341+
flow_run.id,
339342
name=f"harvest-{instrument_name}",
340343
)
341344
except Exception:

backend/app/models/access.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
from __future__ import annotations
2+
13
import enum
24
import uuid
35
from datetime import datetime
6+
from typing import TYPE_CHECKING
47

58
from sqlalchemy import DateTime, Enum, ForeignKey, func
69
from sqlalchemy.orm import Mapped, mapped_column, relationship
710

811
from app.models.base import Base, UUIDPrimaryKey
912

13+
if TYPE_CHECKING:
14+
from app.models.file import FileRecord
15+
1016

1117
class GranteeType(enum.StrEnum):
1218
user = "user"
@@ -22,4 +28,4 @@ class FileAccessGrant(UUIDPrimaryKey, Base):
2228
grantee_id: Mapped[uuid.UUID] = mapped_column()
2329
granted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
2430

25-
file: Mapped["FileRecord"] = relationship(back_populates="access_grants") # noqa: F821
31+
file: Mapped[FileRecord] = relationship(back_populates="access_grants")

backend/app/models/file.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1+
from __future__ import annotations
2+
13
import enum
24
import uuid
35
from datetime import datetime
6+
from typing import TYPE_CHECKING
47

58
from sqlalchemy import JSON, BigInteger, DateTime, Enum, ForeignKey, Index, String
69
from sqlalchemy.orm import Mapped, mapped_column, relationship
710

811
from app.models.base import Base, UUIDPrimaryKey
912

13+
if TYPE_CHECKING:
14+
from app.models.access import FileAccessGrant
15+
from app.models.instrument import Instrument
16+
from app.models.transfer import FileTransfer
17+
1018

1119
class PersistentIdType(enum.StrEnum):
1220
ark = "ark"
@@ -34,10 +42,8 @@ class FileRecord(UUIDPrimaryKey, Base):
3442
metadata_: Mapped[dict | None] = mapped_column("metadata", JSON)
3543
owner_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("users.id"), nullable=True)
3644

37-
instrument: Mapped["Instrument"] = relationship(back_populates="files") # noqa: F821
38-
transfers: Mapped[list["FileTransfer"]] = relationship( # noqa: F821
39-
back_populates="file"
40-
)
41-
access_grants: Mapped[list["FileAccessGrant"]] = relationship( # noqa: F821
45+
instrument: Mapped[Instrument] = relationship(back_populates="files")
46+
transfers: Mapped[list[FileTransfer]] = relationship(back_populates="file")
47+
access_grants: Mapped[list[FileAccessGrant]] = relationship(
4248
back_populates="file", cascade="all, delete-orphan"
4349
)

backend/app/models/hook.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
from __future__ import annotations
2+
13
import enum
24
import uuid
5+
from typing import TYPE_CHECKING
36

47
from sqlalchemy import JSON, Boolean, Enum, ForeignKey, Integer, String, Text
58
from sqlalchemy.orm import Mapped, mapped_column, relationship
69

710
from app.models.base import Base, UUIDPrimaryKey
811

12+
if TYPE_CHECKING:
13+
from app.models.instrument import Instrument
14+
915

1016
class HookTrigger(enum.StrEnum):
1117
pre_transfer = "pre_transfer"
@@ -33,4 +39,4 @@ class HookConfig(UUIDPrimaryKey, Base):
3339
priority: Mapped[int] = mapped_column(Integer, default=0)
3440
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
3541

36-
instrument: Mapped["Instrument | None"] = relationship(back_populates="hooks") # noqa: F821
42+
instrument: Mapped[Instrument | None] = relationship(back_populates="hooks")

backend/app/models/instrument.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1+
from __future__ import annotations
2+
13
import enum
24
import uuid
5+
from typing import TYPE_CHECKING
36

47
from sqlalchemy import JSON, Boolean, Enum, ForeignKey, String, Text
58
from sqlalchemy.orm import Mapped, mapped_column, relationship
69

710
from app.models.base import Base, TimestampMixin, UUIDPrimaryKey
811

12+
if TYPE_CHECKING:
13+
from app.models.file import FileRecord
14+
from app.models.hook import HookConfig
15+
from app.models.schedule import HarvestSchedule
16+
917

1018
class TransferAdapterType(enum.StrEnum):
1119
rclone = "rclone"
@@ -21,7 +29,7 @@ class ServiceAccount(UUIDPrimaryKey, TimestampMixin, Base):
2129
username: Mapped[str] = mapped_column(String(255))
2230
password_encrypted: Mapped[str] = mapped_column(Text)
2331

24-
instruments: Mapped[list["Instrument"]] = relationship(back_populates="service_account")
32+
instruments: Mapped[list[Instrument]] = relationship(back_populates="service_account")
2533

2634

2735
class Instrument(UUIDPrimaryKey, TimestampMixin, Base):
@@ -44,8 +52,6 @@ class Instrument(UUIDPrimaryKey, TimestampMixin, Base):
4452
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
4553

4654
service_account: Mapped[ServiceAccount | None] = relationship(back_populates="instruments")
47-
schedules: Mapped[list["HarvestSchedule"]] = relationship( # noqa: F821
48-
back_populates="instrument"
49-
)
50-
files: Mapped[list["FileRecord"]] = relationship(back_populates="instrument") # noqa: F821
51-
hooks: Mapped[list["HookConfig"]] = relationship(back_populates="instrument") # noqa: F821
55+
schedules: Mapped[list[HarvestSchedule]] = relationship(back_populates="instrument")
56+
files: Mapped[list[FileRecord]] = relationship(back_populates="instrument")
57+
hooks: Mapped[list[HookConfig]] = relationship(back_populates="instrument")

backend/app/models/schedule.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
from __future__ import annotations
2+
13
import uuid
4+
from typing import TYPE_CHECKING
25

36
from sqlalchemy import Boolean, ForeignKey, String
47
from sqlalchemy.orm import Mapped, mapped_column, relationship
58

69
from app.models.base import Base, TimestampMixin, UUIDPrimaryKey
710

11+
if TYPE_CHECKING:
12+
from app.models.instrument import Instrument
13+
from app.models.storage import StorageLocation
14+
815

916
class HarvestSchedule(UUIDPrimaryKey, TimestampMixin, Base):
1017
__tablename__ = "harvest_schedules"
@@ -17,5 +24,5 @@ class HarvestSchedule(UUIDPrimaryKey, TimestampMixin, Base):
1724
prefect_deployment_id: Mapped[str | None] = mapped_column(String(255))
1825
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
1926

20-
instrument: Mapped["Instrument"] = relationship(back_populates="schedules") # noqa: F821
21-
default_storage_location: Mapped["StorageLocation"] = relationship() # noqa: F821
27+
instrument: Mapped[Instrument] = relationship(back_populates="schedules")
28+
default_storage_location: Mapped[StorageLocation] = relationship()

backend/app/models/transfer.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
from __future__ import annotations
2+
13
import enum
24
import uuid
35
from datetime import datetime
6+
from typing import TYPE_CHECKING
47

58
from sqlalchemy import BigInteger, Boolean, DateTime, Enum, ForeignKey, String, Text
69
from sqlalchemy.orm import Mapped, mapped_column, relationship
710

811
from app.models.base import Base, UUIDPrimaryKey
912
from app.models.instrument import TransferAdapterType
1013

14+
if TYPE_CHECKING:
15+
from app.models.file import FileRecord
16+
from app.models.storage import StorageLocation
17+
1118

1219
class TransferStatus(enum.StrEnum):
1320
pending = "pending"
@@ -36,5 +43,5 @@ class FileTransfer(UUIDPrimaryKey, Base):
3643
error_message: Mapped[str | None] = mapped_column(Text)
3744
prefect_flow_run_id: Mapped[str | None] = mapped_column(String(255))
3845

39-
file: Mapped["FileRecord"] = relationship(back_populates="transfers") # noqa: F821
40-
storage_location: Mapped["StorageLocation"] = relationship() # noqa: F821
46+
file: Mapped[FileRecord] = relationship(back_populates="transfers")
47+
storage_location: Mapped[StorageLocation] = relationship()

0 commit comments

Comments
 (0)