CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / TypeScript typecheck (push) Successful in 41s
CI & Build / integration (push) Successful in 37s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 40s
A forge token is a user's credential, not an instance's. The single admin-settings config is replaced by per-user keyring rows (one per forge host), and every server-side forge read runs on the PROJECT OWNER's keyring: - forge_connections table + projects.forge_connection_id pin (migration 0078, which also carries the existing admin config into the first admin's row and deletes the old setting keys — no legacy dual-read) - get_forge() replaced by get_forges(owner_id, project_id) -> ForgeSelector; resolve(repo) picks the connection whose host serves the repo. A pinned project uses ONLY its pinned connection; a stale pin (ownership moved) is ignored, never honored across users - env FORGE_* config survives as an implicit entry for admin owners only; a stored row for the same host beats it - consumers threaded: pull-time freshness (owner of the note), coverage (owner of the project), coverage routes' configured flag - routes: /api/settings/forge-connections CRUD + per-connection test (own-rows only, tokens never returned); /api/admin/forge shrinks to /api/admin/forge-webhook (secret only); PUT /api/projects/<id>/forge pins, owner-or-admin asking, owner's connections only - UI: Git Forges card moves to Settings -> Integrations as a connection list; webhook secret stays in the admin Config tab; owner-only forge select on the project coverage card - backups exclude forge_connections (credentials, api_keys precedent) and the pin, so restores fall back to keyring resolution Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
46 lines
1.7 KiB
Python
46 lines
1.7 KiB
Python
from sqlalchemy import ForeignKey, Integer, Text, UniqueConstraint
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from scribe.models import Base
|
|
from scribe.models.base import TimestampMixin
|
|
|
|
|
|
class ForgeConnection(Base, TimestampMixin):
|
|
"""One user's read-only credential for one git forge host (#2778).
|
|
|
|
The keyring model: a user owns a set of connections and every server-side
|
|
forge read for a project runs on the PROJECT OWNER's set, resolved by the
|
|
repo's host. One row per (user, host) — the repo's host picks the
|
|
connection deterministically, so there is no "default forge" pointer to
|
|
maintain or tie-break.
|
|
|
|
`host` is derived from `base_url` at write time and stored because it is
|
|
the lookup key; the service layer keeps the two in step. The token is a
|
|
secret: to_dict never includes it, and no route may return it.
|
|
"""
|
|
|
|
__tablename__ = "forge_connections"
|
|
__table_args__ = (
|
|
UniqueConstraint("user_id", "host", name="uq_forge_connections_user_host"),
|
|
)
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
user_id: Mapped[int] = mapped_column(
|
|
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
kind: Mapped[str] = mapped_column(Text, nullable=False)
|
|
base_url: Mapped[str] = mapped_column(Text, nullable=False)
|
|
host: Mapped[str] = mapped_column(Text, nullable=False)
|
|
token: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"id": self.id,
|
|
"user_id": self.user_id,
|
|
"kind": self.kind,
|
|
"base_url": self.base_url,
|
|
"host": self.host,
|
|
"created_at": self.created_at.isoformat(),
|
|
"updated_at": self.updated_at.isoformat(),
|
|
}
|