Compare commits

...

15 Commits

Author SHA1 Message Date
Savannah Savings 2089395699 Merge pull request 'fix(ci): migrate Docker registry from GHCR to Gitea' (#14) from barcode-betty/ghcr-to-gitea-registry into dev
CI / deploy-uat (push) Has been skipped
CI / deploy-dev (push) Failing after 41s
CI / lint (push) Successful in 5s
CI / grype (push) Has been skipped
CI / test (push) Successful in 9s
CI / build-and-push (push) Failing after 8s
2026-05-24 18:51:46 +00:00
Savannah Savings a197ab0530 {{.PullRequestTitle}}
CI / deploy-uat (push) Has been skipped
CI / test (push) Successful in 13s
CI / lint (push) Successful in 3s
CI / lint (pull_request) Successful in 3s
CI / test (pull_request) Successful in 14s
CI / build-and-push (push) Failing after 8s
CI / build-and-push (pull_request) Has been skipped
CI / grype (push) Has been skipped
CI / grype (pull_request) Has been skipped
CI / deploy-dev (pull_request) Has been skipped
CI / deploy-uat (pull_request) Has been skipped
CI / deploy-dev (push) Failing after 30s
2026-05-24 18:48:28 +00:00
Barcode Betty da9e1e9af1 ci: migrate GHCR → Gitea registry, use REGISTRY_TOKEN
CI / lint (pull_request) Successful in 5s
CI / deploy-dev (pull_request) Has been skipped
CI / test (pull_request) Successful in 24s
CI / build-and-push (pull_request) Has been skipped
CI / grype (pull_request) Has been skipped
CI / deploy-uat (pull_request) Has been skipped
Fixes CAR-1009.

- REGISTRY already git.farh.net (via CAR-964)
- Change GITEA_TOKEN → REGISTRY_TOKEN for consistency

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-24 18:39:33 +00:00
Barcode Betty 1cf7f92b6b fix(ci): migrate Docker registry from GHCR to Gitea
CI / lint (pull_request) Has been cancelled
CI / test (pull_request) Has been cancelled
CI / build-and-push (pull_request) Has been cancelled
CI / grype (pull_request) Has been cancelled
CI / deploy-dev (pull_request) Has been cancelled
CI / deploy-uat (pull_request) Has been cancelled
- Change REGISTRY from ghcr.io to git.farh.net
- Replace GHCR login with Gitea Container Registry login using REGISTRY_TOKEN
- Move workflow from .github/workflows to .gitea/workflows

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-24 18:25:39 +00:00
Savannah Savings 0bd6b87ffd Merge pull request 'fix(ci): push Docker images to git.farh.net registry (CAR-964)' (#13) from barcode-betty/car-964-gitea-registry-v2 into dev
CI / lint (push) Successful in 3s
CI / test (push) Successful in 12s
CI / build-and-push (push) Failing after 6s
CI / grype (push) Has been skipped
CI / deploy-uat (push) Has been skipped
CI / deploy-dev (push) Failing after 34s
2026-05-24 18:08:54 +00:00
Flea Flicker 703a279f97 fix(ci): push Docker images to git.farh.net registry (CAR-964)
CI / build-and-push (pull_request) Has been skipped
CI / deploy-dev (pull_request) Has been skipped
CI / deploy-uat (pull_request) Has been skipped
CI / lint (pull_request) Failing after 1m33s
CI / test (pull_request) Failing after 1m32s
CI / grype (pull_request) Has been skipped
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-23 16:07:51 +00:00
Coupon Carl ef5102aad9 Merge pull request 'chore: move workflows from .github to .gitea (CAR-900)' (#8) from carl/car-900-move-workflows-to-gitea into dev
CI / test (push) Successful in 8s
CI / build-and-push (push) Has been skipped
CI / lint (push) Failing after 3s
CI / deploy-uat (push) Has been skipped
CI / grype (push) Has been skipped
CI / deploy-dev (push) Failing after 32s
CI / test (pull_request) Failing after 1m34s
CI / deploy-dev (pull_request) Has been skipped
CI / deploy-uat (pull_request) Has been skipped
CI / lint (pull_request) Failing after 1m27s
CI / grype (pull_request) Has been skipped
CI / build-and-push (pull_request) Has been skipped
chore: move workflows from .github to .gitea (CAR-900)
2026-05-21 18:56:01 +00:00
Savannah Savings f63f6c613d chore: move workflows from .github to .gitea
CI / deploy-uat (pull_request) Has been skipped
CI / lint (pull_request) Successful in 3s
CI / test (pull_request) Successful in 8s
CI / build-and-push (pull_request) Has been skipped
CI / grype (pull_request) Has been skipped
CI / deploy-dev (pull_request) Has been skipped
Part of Gitea migration (CAR-900 / CAR-894).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 18:54:46 +00:00
Savannah Savings 377f8428c5 Merge pull request 'ci: convert GitHub Actions to Gitea Actions (ubuntu-latest)' (#6) from betty/car-876-gitea-actions-receiptwitness into dev
CI / lint (push) Successful in 4s
CI / test (push) Successful in 9s
CI / lint (pull_request) Successful in 5s
CI / test (pull_request) Successful in 7s
CI / build-and-push (push) Failing after 6s
CI / build-and-push (pull_request) Has been skipped
CI / grype (push) Has been skipped
CI / deploy-uat (push) Has been skipped
CI / grype (pull_request) Has been skipped
CI / deploy-uat (pull_request) Has been skipped
CI / deploy-dev (pull_request) Has been skipped
CI / deploy-dev (push) Failing after 30s
ci: convert GitHub Actions to Gitea Actions (ubuntu-latest)

CTO-approved. QA passed. Mechanical CI migration.

cc @cpfarhood
2026-05-21 11:55:48 +00:00
Flea Flicker 340b974532 ci: convert GitHub Actions to Gitea Actions (ubuntu-latest)
CI / deploy-dev (pull_request) Has been skipped
CI / build-and-push (pull_request) Has been skipped
CI / grype (pull_request) Has been skipped
CI / deploy-uat (pull_request) Has been skipped
CI / lint (pull_request) Successful in 4s
CI / test (pull_request) Successful in 13s
- Replace runs-on: runners-cartsnitch with runs-on: ubuntu-latest
- Replace GitHub App token auth with secrets.GITEA_TOKEN for cross-repo checkout
- Remove actions/create-github-app-token steps from deploy-dev and deploy-uat

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-21 04:13:14 +00:00
cartsnitch-engineer[bot] 2fd2cdca71 feat: migrate receiptwitness to standalone repo with inlined common
Migrates receiptwitness to a standalone repo with inlined common models. Includes SQLite test compatibility fixes (server_default stripping for gen_random_bytes, relationship stubs for SQLAlchemy mapper).

QA PASS (cartsnitch-qa): lint  test  306/306 passed.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-04 21:01:21 +00:00
savannah-savings-cto[bot] 8128b3a76f chore: promote uat to main — receiptwitness migration CI fixes
Merged to production. UAT regression and security review both passed.

- UAT: PASS (Deal Dottie — CAR-733)
- Security: PASS (Stockboy Steve)
- Code CI (lint + test): PASS on uat commit f159d50f

Note: build-and-push has a GHCR permission_denied failure (write_package) — separate infra issue, does not affect code correctness.
2026-04-19 14:02:18 +00:00
cartsnitch-ceo[bot] f159d50f7c Merge pull request #4 from cartsnitch/dev
chore: promote dev to uat — receiptwitness migration CI fixes
2026-04-19 13:25:40 +00:00
cartsnitch-ceo[bot] c4880d3553 Merge pull request #3 from cartsnitch/betty/car-724-ci-fix
fix: resolve CI failures — SQLite incompatibility and ruff lint errors
2026-04-19 13:25:16 +00:00
Barcode Betty 873f53b9fc fix: resolve CI failures — SQLite incompatibility and ruff lint errors
- Remove PostgreSQL-specific server_default from User.email_inbound_token.
  The column has a Python-side default (secrets.token_urlsafe) that works
  for both SQLite and PostgreSQL. The gen_random_bytes() server_default
  caused sqlite table creation to fail.

- Add missing back_populates relationships to stub models so SQLAlchemy
  mapper configuration succeeds. Purchase.user and Store.user_accounts
  were missing, causing "has no property" errors during Base.metadata.create_all.

- Auto-fix ruff import sorting (I001) across all source and test files.

- Manually fix line-too-long (E501) in config.py.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 13:06:47 +00:00
14 changed files with 75 additions and 55 deletions
@@ -15,12 +15,12 @@ permissions:
packages: write
env:
REGISTRY: ghcr.io
REGISTRY: git.farh.net
IMAGE_NAME: cartsnitch/receiptwitness
jobs:
lint:
runs-on: runners-cartsnitch
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
@@ -33,7 +33,7 @@ jobs:
run: ruff check src/ tests/
test:
runs-on: runners-cartsnitch
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
@@ -46,7 +46,7 @@ jobs:
run: pytest tests/ -v
build-and-push:
runs-on: runners-cartsnitch
runs-on: ubuntu-latest
if: github.event_name == 'push'
needs: [lint, test]
outputs:
@@ -69,12 +69,12 @@ jobs:
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "CalVer tag: $VERSION"
- name: Log in to GHCR
- name: Log in to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Extract metadata
id: meta
@@ -102,7 +102,7 @@ jobs:
git push origin "v${{ steps.calver.outputs.version }}"
grype:
runs-on: runners-cartsnitch
runs-on: ubuntu-latest
needs: [build-and-push]
if: github.event_name == 'push'
steps:
@@ -126,24 +126,15 @@ jobs:
ignore-file: .grype.yaml
deploy-dev:
runs-on: runners-cartsnitch
runs-on: ubuntu-latest
needs: [grype]
if: always() && !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/dev'
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CARTSNITCH_APP_ID }}
private-key: ${{ secrets.CARTSNITCH_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: infra
- name: Checkout infra repo
uses: actions/checkout@v4
with:
repository: cartsnitch/infra
token: ${{ steps.app-token.outputs.token }}
token: ${{ secrets.GITEA_TOKEN }}
ref: main
path: infra
@@ -156,7 +147,7 @@ jobs:
- name: Update receiptwitness image tag
run: |
cd infra/apps/overlays/dev
kustomize edit set image ghcr.io/cartsnitch/receiptwitness:sha-${{ github.sha }}
kustomize edit set image git.farh.net/cartsnitch/receiptwitness:sha-${{ github.sha }}
- name: Commit and push to infra
run: |
@@ -169,24 +160,15 @@ jobs:
git push origin main
deploy-uat:
runs-on: runners-cartsnitch
runs-on: ubuntu-latest
needs: [grype]
if: always() && !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/uat'
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.CARTSNITCH_APP_ID }}
private-key: ${{ secrets.CARTSNITCH_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: infra
- name: Checkout infra repo
uses: actions/checkout@v4
with:
repository: cartsnitch/infra
token: ${{ steps.app-token.outputs.token }}
token: ${{ secrets.GITEA_TOKEN }}
ref: main
path: infra
@@ -199,7 +181,7 @@ jobs:
- name: Update receiptwitness image tag
run: |
cd infra/apps/overlays/uat
kustomize edit set image ghcr.io/cartsnitch/receiptwitness:${{ needs.build-and-push.outputs.calver_tag }}
kustomize edit set image git.farh.net/cartsnitch/receiptwitness:${{ needs.build-and-push.outputs.calver_tag }}
- name: Commit and push to infra
run: |
+2 -2
View File
@@ -3,7 +3,6 @@
from pydantic import model_validator
from pydantic_settings import BaseSettings
_PLACEHOLDER_VALUES = {"change-me-in-production"}
@@ -40,7 +39,8 @@ class ReceiptWitnessSettings(BaseSettings):
if not self.session_encryption_key or self.session_encryption_key in _PLACEHOLDER_VALUES:
errors.append(
"RW_SESSION_ENCRYPTION_KEY must be set to a secure value. "
'Generate one with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"'
"Generate one with: python -c "
'"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"'
)
if self.notifications_enabled and not self.resend_api_key:
errors.append(
+2 -2
View File
@@ -7,12 +7,12 @@ from datetime import UTC, datetime
from decimal import Decimal
import redis.asyncio as aioredis
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
from sqlalchemy import select
from receiptwitness.config import settings
from receiptwitness.notifications.email import send_receipt_notification
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
logger = logging.getLogger(__name__)
+3 -3
View File
@@ -7,9 +7,6 @@ and batch matching for purchase ingestion.
import uuid
from dataclasses import dataclass
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
from sqlalchemy.orm import Session
from receiptwitness.pipeline.normalization import (
@@ -18,6 +15,9 @@ from receiptwitness.pipeline.normalization import (
extract_size_info,
normalize_product,
)
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
# Re-export for convenience
ConfidenceLevel = MatchConfidence
+3 -2
View File
@@ -10,11 +10,12 @@ import re
from dataclasses import dataclass
from enum import StrEnum
from receiptwitness.shared.models import NormalizedProduct
from sqlalchemy import cast, func, select, String
from sqlalchemy import String, cast, func, select
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Session
from receiptwitness.shared.models import NormalizedProduct
class MatchMethod(StrEnum):
"""How a product match was determined."""
+2 -2
View File
@@ -2,12 +2,12 @@
from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
from receiptwitness.shared.models.product import NormalizedProduct
from receiptwitness.shared.models.user import User, UserStoreAccount
from receiptwitness.shared.models.stub_purchase import Purchase, PurchaseItem
# Stub models — needed for relationship resolution but not directly used by receiptwitness.
# Full definitions live in cartsnitch/common.
from receiptwitness.shared.models.stub_store import Store, StoreLocation
from receiptwitness.shared.models.stub_purchase import Purchase, PurchaseItem
from receiptwitness.shared.models.user import User, UserStoreAccount
__all__ = [
"Base",
@@ -8,12 +8,26 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common.
import uuid
from datetime import date, datetime
from decimal import Decimal
from typing import TYPE_CHECKING
from sqlalchemy import JSON, Date, DateTime, ForeignKey, Index, Numeric, String, UniqueConstraint, func
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import (
JSON,
Date,
DateTime,
ForeignKey,
Index,
Numeric,
String,
UniqueConstraint,
func,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
if TYPE_CHECKING:
from receiptwitness.shared.models.user import User
class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Stub: a shopping trip/receipt. Full definition in cartsnitch/common."""
@@ -42,6 +56,8 @@ class Purchase(UUIDPrimaryKeyMixin, TimestampMixin, Base):
UniqueConstraint("user_id", "store_id", "receipt_id", name="uq_purchase_receipt"),
)
user: Mapped["User"] = relationship(back_populates="purchases")
class PurchaseItem(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Stub: a line item on a receipt. Full definition in cartsnitch/common."""
@@ -6,6 +6,7 @@ UserStoreAccount. The canonical definitions live in cartsnitch/common.
"""
import uuid
from typing import TYPE_CHECKING
from sqlalchemy import Float, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -13,6 +14,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from receiptwitness.shared.constants import StoreSlug
from receiptwitness.shared.models.base import Base, TimestampMixin, UUIDPrimaryKeyMixin
if TYPE_CHECKING:
from receiptwitness.shared.models.user import UserStoreAccount
class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Stub: canonical retailer. Full definition in cartsnitch/common."""
@@ -24,6 +28,8 @@ class Store(UUIDPrimaryKeyMixin, TimestampMixin, Base):
logo_url: Mapped[str | None] = mapped_column(String(500))
website_url: Mapped[str | None] = mapped_column(String(500))
user_accounts: Mapped[list["UserStoreAccount"]] = relationship(back_populates="store")
class StoreLocation(UUIDPrimaryKeyMixin, TimestampMixin, Base):
"""Stub: physical store location. Full definition in cartsnitch/common."""
+1 -1
View File
@@ -28,7 +28,7 @@ class User(UUIDPrimaryKeyMixin, TimestampMixin, Base):
unique=True,
default=lambda: secrets.token_urlsafe(16),
server_default=text(
"replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')"
"replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')" # noqa: E501
),
)
hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True)
+2 -2
View File
@@ -3,8 +3,6 @@
import asyncio
import logging
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
from sqlalchemy import select
from receiptwitness.config import settings
@@ -15,6 +13,8 @@ from receiptwitness.parsers.email.kroger import KrogerEmailParser
from receiptwitness.parsers.email.meijer import MeijerEmailParser
from receiptwitness.parsers.email.target import TargetEmailParser
from receiptwitness.queue.email import ack_email, consume_emails, get_redis
from receiptwitness.shared.database import get_async_session_factory
from receiptwitness.shared.models import User
logger = logging.getLogger(__name__)
+1
View File
@@ -1,4 +1,5 @@
import pytest
from receiptwitness.config import ReceiptWitnessSettings
+18 -2
View File
@@ -1,15 +1,31 @@
"""Shared test fixtures for pipeline tests."""
import secrets
import pytest
from receiptwitness.shared.models import Base
from sqlalchemy import create_engine
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from receiptwitness.shared.models import Base
from receiptwitness.shared.models.user import User
@event.listens_for(User, "before_insert")
def _populate_email_inbound_token(mapper, connection, target):
"""Populate email_inbound_token with a secure random value when unset.
SQLite has no gen_random_bytes() function, so we generate it in Python
instead of relying on the PostgreSQL server_default.
"""
if target.email_inbound_token is None:
target.email_inbound_token = secrets.token_urlsafe(16)
@pytest.fixture
def engine():
"""In-memory SQLite engine for unit tests."""
eng = create_engine("sqlite:///:memory:")
User.__table__.c.email_inbound_token.server_default = None
Base.metadata.create_all(eng)
yield eng
eng.dispose()
+3 -4
View File
@@ -4,16 +4,15 @@ import uuid
from datetime import UTC, datetime
from decimal import Decimal
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
from receiptwitness.pipeline.matching import (
ProductMatcher,
classify_confidence,
match_purchase_item,
)
from receiptwitness.pipeline.normalization import MatchMethod
from receiptwitness.shared.constants import MatchConfidence
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.shared.schemas import PurchaseItemCreate
class TestClassifyConfidence:
+1 -2
View File
@@ -3,8 +3,6 @@
import uuid
from datetime import UTC, datetime
from receiptwitness.shared.models import NormalizedProduct
from receiptwitness.pipeline.normalization import (
MatchMethod,
clean_name,
@@ -14,6 +12,7 @@ from receiptwitness.pipeline.normalization import (
match_by_upc,
normalize_product,
)
from receiptwitness.shared.models import NormalizedProduct
class TestCleanName: