Skip to content
Merged

Dev #33

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/workflows/sdk-python.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: sdk-python

# Two triggers:
# - push to main touching sdks/python/** → runs the test job only.
# - a git tag of the form sdk-python-vX.Y.Z → runs tests AND publishes
# to PyPI via trusted publishing (OIDC, no long-lived token).
#
# Tag convention: ``git tag sdk-python-v0.1.0 && git push origin
# sdk-python-v0.1.0``. The version in the tag must match the version in
# pyproject.toml; the publish step fails fast if they disagree.

on:
push:
branches: [main]
paths:
- "sdks/python/**"
- ".github/workflows/sdk-python.yml"
tags:
- "sdk-python-v*"
pull_request:
paths:
- "sdks/python/**"
- ".github/workflows/sdk-python.yml"

concurrency:
group: sdk-python-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12"]
defaults:
run:
working-directory: sdks/python
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip
- run: pip install --upgrade pip
- run: pip install -e ".[dev]"
- name: Lint
run: ruff check .
- name: Test
run: pytest -q

publish:
if: startsWith(github.ref, 'refs/tags/sdk-python-v')
needs: test
runs-on: ubuntu-latest
defaults:
run:
working-directory: sdks/python
# Trusted publishing — configure once on PyPI:
# https://docs.pypi.org/trusted-publishers/
# Repository owner + name + workflow filename + environment all must
# match what's configured there. No secrets live in the repo.
environment: pypi-release
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install build twine
- name: Verify tag matches pyproject version
run: |
TAG_VERSION="${GITHUB_REF_NAME#sdk-python-v}"
PKG_VERSION="$(python -c 'import tomllib; print(tomllib.loads(open("pyproject.toml","rb").read().decode())["project"]["version"])')"
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "::error::Tag version $TAG_VERSION does not match pyproject version $PKG_VERSION"
exit 1
fi
- name: Build
run: python -m build
- name: Check
run: twine check dist/*
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: sdks/python/dist
82 changes: 82 additions & 0 deletions .github/workflows/sdk-typescript.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: sdk-typescript

# Two triggers:
# - push / PR touching sdks/typescript/** → test + typecheck.
# - tag sdk-typescript-vX.Y.Z → tests then publishes to npm with
# provenance (SLSA-style attestation visible on the npm page).
#
# Tag convention: ``git tag sdk-typescript-v0.1.0 && git push origin
# sdk-typescript-v0.1.0``. The tag version must match package.json; the
# publish job fails fast if they disagree.

on:
push:
branches: [main]
paths:
- "sdks/typescript/**"
- ".github/workflows/sdk-typescript.yml"
tags:
- "sdk-typescript-v*"
pull_request:
paths:
- "sdks/typescript/**"
- ".github/workflows/sdk-typescript.yml"

concurrency:
group: sdk-typescript-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ["18", "20", "22"]
defaults:
run:
working-directory: sdks/typescript
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
cache-dependency-path: sdks/typescript/package-lock.json
- run: npm ci
- run: npm run typecheck
- run: npm test

publish:
if: startsWith(github.ref, 'refs/tags/sdk-typescript-v')
needs: test
runs-on: ubuntu-latest
defaults:
run:
working-directory: sdks/typescript
# npm provenance requires id-token:write. The NPM_TOKEN secret is a
# classic automation token with publish permission on @opengraph/sdk.
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"
cache: npm
cache-dependency-path: sdks/typescript/package-lock.json
- run: npm ci
- name: Verify tag matches package.json version
run: |
TAG_VERSION="${GITHUB_REF_NAME#sdk-typescript-v}"
PKG_VERSION="$(node -p "require('./package.json').version")"
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "::error::Tag version $TAG_VERSION does not match package.json version $PKG_VERSION"
exit 1
fi
- run: npm run build
- name: Publish to npm with provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --provenance --access public
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,8 @@ htmlcov/
frontend/node_modules/
frontend/.next/
frontend/tsconfig.tsbuildinfo

# SDK build artifacts
sdks/typescript/node_modules/
sdks/typescript/dist/
sdks/python/.pytest_cache/
56 changes: 56 additions & 0 deletions alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Alembic configuration for OpenGraph.
#
# The legacy idempotent ALTER TABLE IF NOT EXISTS bootstrap in
# ``src/infra/db.py::init_db`` still runs on every startup for zero-downtime
# dev ergonomics. Alembic is the strict, versioned migration path for prod:
#
# alembic upgrade head # apply pending migrations
# alembic revision --autogenerate -m "add_foo_column"
# alembic downgrade -1 # rollback one step
# alembic stamp head # mark DB at HEAD without running migrations
# # (first-time adoption on an existing DB)
#
# env.py reads DATABASE_URL from src.config so the same URL drives both the
# runtime engine and the migration engine.

[alembic]
script_location = alembic
prepend_sys_path = .
version_path_separator = os
sqlalchemy.url =

[post_write_hooks]

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
75 changes: 75 additions & 0 deletions alembic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Alembic migrations

OpenGraph uses Alembic for versioned schema changes. The legacy idempotent
`ALTER TABLE IF NOT EXISTS` bootstrap in `src/infra/db.py::init_db` still
runs on every startup — that path is fine for dev but not safe for
1M-row alterations in prod.

## First-time adoption on an existing database

If Neon already holds the current schema (everything `init_db` creates),
stamp the DB at HEAD so Alembic doesn't try to re-create tables:

```bash
alembic stamp head
```

That writes a row into `alembic_version` and skips over the existing
schema. From there forward, every schema change is a migration.

## Creating a new migration

```bash
alembic revision --autogenerate -m "add_my_column"
# review the generated versions/*.py, tweak the batching / defaults
alembic upgrade head
```

Autogenerate picks up new/changed columns, indexes and constraints from
`src.infra.db_models.Base.metadata`. It will *not* detect:

- Server-side DEFAULT changes that aren't part of the ORM default.
- Data backfills (you have to write them by hand in `upgrade()`).
- Check constraint wording changes.

Always re-read the diff before applying.

## Safe patterns for 1M-row tables

Adding a NOT NULL column with a default:

```python
def upgrade() -> None:
op.add_column("workspaces", sa.Column("plan_tier", sa.String(16), nullable=True))
op.execute("UPDATE workspaces SET plan_tier = 'trial' WHERE plan_tier IS NULL")
op.alter_column("workspaces", "plan_tier", nullable=False)
```

Three small transactions beat one giant ALTER that locks writes for
minutes.

Adding an index on a hot table:

```python
def upgrade() -> None:
op.create_index(
"ix_chat_messages_session",
"chat_messages",
["session_id"],
postgresql_concurrently=True,
)
```

`CONCURRENTLY` keeps writes unblocked; the trade-off is you can't wrap
it in a transaction (Alembic handles this automatically when the flag
is set).

## Downgrades

```bash
alembic downgrade -1
```

Write `downgrade()` to drop whatever `upgrade()` added. For destructive
migrations (data deletion, column drops) leave a comment making the
point of no return obvious.
Loading
Loading