Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
068287e
chore(spec-sync): add fetch/normalize script and V1 spec snapshot bas…
yzld2002 Jul 7, 2026
92ee58b
chore(spec-sync): add drift detector
yzld2002 Jul 7, 2026
ee48f45
chore(spec-sync): add reference-model codegen (datamodel-code-generator)
yzld2002 Jul 7, 2026
0d98f60
chore(spec-sync): add griffe surface-lock gate (baseline=last release…
yzld2002 Jul 7, 2026
9ce063a
test(spec-sync): add staging contract-test scaffold (contract marker)
yzld2002 Jul 7, 2026
af14062
ci(spec-sync): add two-phase spec-sync workflow (mechanical + AI)
yzld2002 Jul 7, 2026
abddece
ci(spec-sync): add PR gates (surface-lock + staging contract tests)
yzld2002 Jul 7, 2026
40d8efe
ci(spec-sync): add production-spec release gate (staging-in, producti…
yzld2002 Jul 7, 2026
c30c495
docs(spec-sync): document the spec-sync pipeline and TS-port follow-up
yzld2002 Jul 7, 2026
2afbbb6
fix(spec-sync): exclude generated specs from linters; lock deps; stag…
yzld2002 Jul 7, 2026
98e7bb8
fix(spec-sync): pin codegen/griffe to validated versions; regen model…
yzld2002 Jul 7, 2026
93666d5
ci(spec-sync): use fine-grained PAT (SPEC_SYNC_TOKEN) instead of a Gi…
yzld2002 Jul 7, 2026
e94036c
fix(spec-sync): method-granular release gate; mkdir -p snapshot parent
yzld2002 Jul 7, 2026
0ad5bea
chore(spec-sync): re-baseline V1 snapshot to the SDK's implemented su…
yzld2002 Jul 7, 2026
dfce66d
ci(spec-sync): deterministic AI push, no duplicate PRs, injection har…
yzld2002 Jul 8, 2026
513fb06
ci(spec-sync): scope live-staging gate, exact codegen pins, opt-in co…
yzld2002 Jul 8, 2026
5942ca6
fix(spec-sync): close staging client in fixture; align docs to hourly…
yzld2002 Jul 8, 2026
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
43 changes: 43 additions & 0 deletions .github/workflows/pr-gates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: PR Gates
on:
pull_request:
branches: [main]

jobs:
surface-lock:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # tags for the release-tag baseline
- name: Install Rye
run: |
curl -sSf https://rye.astral.sh/get | bash
echo "$HOME/.rye/shims" >> "$GITHUB_PATH"
env:
RYE_VERSION: '0.44.0'
RYE_INSTALL_OPTION: '--yes'
- name: Install dependencies
run: rye sync --all-features
- name: Surface lock (no breaking change to released API)
run: ./scripts/spec-sync/surface-lock.sh

contract-tests:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Install Rye
run: |
curl -sSf https://rye.astral.sh/get | bash
echo "$HOME/.rye/shims" >> "$GITHUB_PATH"
env:
RYE_VERSION: '0.44.0'
RYE_INSTALL_OPTION: '--yes'
- name: Install dependencies
run: rye sync --all-features
- name: Contract tests vs staging
env:
LANDINGAI_ADE_STAGING_APIKEY: ${{ secrets.LANDINGAI_ADE_STAGING_APIKEY }}
run: rye run pytest tests/contract -m contract -v
Comment thread
yzld2002 marked this conversation as resolved.
Outdated
Comment thread
yzld2002 marked this conversation as resolved.
Outdated
Comment thread
yzld2002 marked this conversation as resolved.
Outdated
98 changes: 98 additions & 0 deletions .github/workflows/spec-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Spec Sync
on:
schedule:
- cron: '0 */6 * * *' # every ~6h; cron covers correctness, dispatch cuts latency
Comment thread
yzld2002 marked this conversation as resolved.
Outdated
workflow_dispatch: {}

permissions:
contents: read # writes happen via the app token, not GITHUB_TOKEN

jobs:
spec-sync:
if: github.repository == 'landing-ai/ade-python'
Comment thread
yzld2002 marked this conversation as resolved.
runs-on: ubuntu-latest
timeout-minutes: 30
env:
V1_SPEC_URL: https://api.va.staging.landing.ai/v1/ade/openapi.json # staging drives the loop
steps:
- uses: actions/checkout@v6
with:
# SPEC_SYNC_TOKEN is a fine-grained PAT (Contents: RW, Pull requests: RW)
# scoped to this repo. It must NOT be the default GITHUB_TOKEN: pushes/PRs
# authored by GITHUB_TOKEN do not trigger the gate workflows (anti-recursion).
token: ${{ secrets.SPEC_SYNC_TOKEN }}
fetch-depth: 0 # tags needed for surface-lock baseline

- name: Install Rye
run: |
curl -sSf https://rye.astral.sh/get | bash
echo "$HOME/.rye/shims" >> "$GITHUB_PATH"
env:
RYE_VERSION: '0.44.0'
RYE_INSTALL_OPTION: '--yes'

- name: Install dependencies
run: rye sync --all-features

- name: Detect drift
id: drift
run: |
set +e
./scripts/spec-sync/check-drift.sh "$V1_SPEC_URL" specs/v1-ade.json
echo "code=$?" >> "$GITHUB_OUTPUT"

- name: No drift
if: steps.drift.outputs.code == '0'
run: echo "specs in sync; nothing to do."

- name: Fail on fetch error
if: steps.drift.outputs.code != '0' && steps.drift.outputs.code != '10'
run: |
echo "spec fetch/normalize failed (exit ${{ steps.drift.outputs.code }})"
exit 1

# ---- Phase 1: mechanical ----
- name: Mechanical commit (snapshot + reference models)
if: steps.drift.outputs.code == '10'
id: mech
run: |
git config user.name "spec-sync[bot]"
git config user.email "spec-sync@users.noreply.github.com"
branch="spec-sync/v1-$(date -u +%Y%m%dT%H%M%SZ)"
Comment thread
yzld2002 marked this conversation as resolved.
Outdated
git checkout -b "$branch"
./scripts/spec-sync/gen-models.sh specs/v1-ade.json specs/_generated/v1_models.py
git add specs/v1-ade.json specs/_generated/v1_models.py
git commit -m "chore(spec-sync): update V1 spec snapshot + regenerated reference models"
git push -u origin "$branch"
echo "branch=$branch" >> "$GITHUB_OUTPUT"

# ---- Phase 2: AI wiring (same checkout/branch) ----
- name: AI wiring commit
if: steps.drift.outputs.code == '10'
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ secrets.SPEC_SYNC_TOKEN }}
prompt: |
The previous commit updated specs/v1-ade.json and regenerated reference
models in specs/_generated/v1_models.py. Update the SDK to match the spec
diff: add or adjust resource classes, method signatures, param TypedDicts,
response models, tests, api.md, and README examples, mirroring the existing
conventions in src/landingai_ade/resources/parse_jobs.py.
Rules: PURELY ADDITIVE — never modify or remove an existing public signature
(the surface-lock CI job will fail the PR); follow existing code style;
run ./scripts/format; commit to the current branch.
Comment thread
yzld2002 marked this conversation as resolved.
Outdated
claude_args: |
--max-turns 40
--allowedTools "Edit,Write,Read,Bash(git *),Bash(rye *),Bash(./scripts/*)"
Comment thread
yzld2002 marked this conversation as resolved.
Outdated

- name: Open sync PR
Comment thread
yzld2002 marked this conversation as resolved.
Outdated
if: steps.drift.outputs.code == '10'
env:
GH_TOKEN: ${{ secrets.SPEC_SYNC_TOKEN }}
run: |
gh pr create \
--base main \
--head "${{ steps.mech.outputs.branch }}" \
--title "spec-sync: track V1 spec drift" \
--body $'Automated spec-sync PR.\n\n- **Commit 1 (mechanical):** normalized spec snapshot + regenerated reference models.\n- **Commit 2 (AI):** resources/methods/tests/docs wired from the spec diff.\n\nGates (surface-lock, contract tests, lint/test/typecheck) must pass. **Human review required before merge.**'
41 changes: 41 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,44 @@ You can release to package managers by using [the `Publish PyPI` GitHub action](

If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on
the environment.

## Spec-sync pipeline

The SDK tracks the live ADE OpenAPI spec automatically via `.github/workflows/spec-sync.yml`
(cron ~6h + manual `workflow_dispatch`). It is driven by the **staging** spec; releases gate on
the **production** spec ("staging in, production out").
Comment thread
yzld2002 marked this conversation as resolved.

On each run it fetches and normalizes the live spec (`scripts/spec-sync/fetch-normalize.sh`) and
diffs it against the committed snapshot `specs/v1-ade.json` (`scripts/spec-sync/check-drift.sh`).
On drift it opens one PR with two attributed commits:

1. **Mechanical** — updated `specs/v1-ade.json` snapshot plus regenerated *reference* models in
`specs/_generated/v1_models.py` (`scripts/spec-sync/gen-models.sh`, `datamodel-code-generator`).
These reference models are an input for the AI step and for review — they are **not** shipped and
do **not** replace `src/landingai_ade/types/*`.
2. **AI** — `anthropics/claude-code-action` (automation mode) wires the resources, methods, param
types, tests, and docs from the spec diff, following existing conventions.

Every spec-sync PR (and any PR to `main`) must pass `.github/workflows/pr-gates.yml`:

- **surface-lock** (`scripts/spec-sync/surface-lock.sh`, `griffe`) — baseline is the **last release
tag**, so any change to *released* public surface fails mechanically. Merged-but-unreleased surface
stays mutable.
- **contract-tests** — `tests/contract` (marker `contract`) run against staging when
`LANDINGAI_ADE_STAGING_APIKEY` is set; skipped otherwise.

Spec-sync PRs are AI-drafted and **require human review** before merge.

**Secrets required:** `SPEC_SYNC_TOKEN` (a fine-grained PAT scoped to this repo with
`Contents: Read and write` and `Pull requests: Read and write`), `ANTHROPIC_API_KEY`, and
`LANDINGAI_ADE_STAGING_APIKEY`. `SPEC_SYNC_TOKEN` must **not** be the default `GITHUB_TOKEN`:
pushes and PRs authored by `GITHUB_TOKEN` do not trigger the gate workflows (GitHub
anti-recursion), so the gates would never run on the sync PR. A GitHub App installation token
(org-owned) is the cleaner long-term choice and can replace the PAT without other workflow
changes.
Comment thread
yzld2002 marked this conversation as resolved.

**V2:** once the aide gateway serves its curated spec unauthenticated, add `specs/v2-aide.json` and
its URL as a second drift-check in the workflow; the two-phase machinery is unchanged.

The same pipeline shape ports to `ade-typescript` with `openapi-typescript` (mechanical) and
`api-extractor` (surface-lock), tracked separately.
11 changes: 10 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ dev-dependencies = [
"importlib-metadata>=6.7.0",
"rich>=13.7.1",
"pytest-xdist>=3.6.1",
"datamodel-code-generator>=0.45",
Comment thread
yzld2002 marked this conversation as resolved.
Outdated
"griffe>=1.14",
]
Comment thread
yzld2002 marked this conversation as resolved.

[tool.rye.scripts]
Expand Down Expand Up @@ -134,6 +136,9 @@ addopts = "--tb=short -n auto"
xfail_strict = true
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
markers = [
"contract: hits the live staging API; requires LANDINGAI_ADE_STAGING_APIKEY",
]
Comment thread
yzld2002 marked this conversation as resolved.
filterwarnings = [
"error"
]
Expand All @@ -150,6 +155,7 @@ exclude = [
".venv",
".nox",
".git",
"specs",
]

reportImplicitOverride = true
Expand All @@ -168,7 +174,7 @@ show_error_codes = true
#
# We also exclude our `tests` as mypy doesn't always infer
# types correctly and Pyright will still catch any type errors.
exclude = ['src/landingai_ade/_files.py', '_dev/.*.py', 'tests/.*']
exclude = ['src/landingai_ade/_files.py', '_dev/.*.py', 'tests/.*', 'specs/.*']

strict_equality = true
implicit_reexport = true
Expand Down Expand Up @@ -214,6 +220,9 @@ ignore_missing_imports = true
line-length = 120
output-format = "grouped"
target-version = "py38"
# specs/_generated holds datamodel-code-generator reference models (not shipped);
# they are not held to the SDK's lint standards.
extend-exclude = ["specs"]

[tool.ruff.format]
docstring-code-format = true
Expand Down
39 changes: 39 additions & 0 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ anyio==4.12.1
# via httpx
# via landingai-ade
argcomplete==3.6.3
# via datamodel-code-generator
# via nox
async-timeout==5.0.1
# via aiohttp
Expand All @@ -31,11 +32,18 @@ attrs==25.4.0
# via nox
backports-asyncio-runner==1.2.0
# via pytest-asyncio
black==25.11.0
# via datamodel-code-generator
certifi==2026.1.4
# via httpcore
# via httpx
click==8.1.8
# via black
colorama==0.4.6
# via griffe
colorlog==6.10.1
# via nox
datamodel-code-generator==0.45.0
dependency-groups==1.3.1
# via nox
dirty-equals==0.11
Expand All @@ -53,6 +61,9 @@ filelock==3.19.1
frozenlist==1.8.0
# via aiohttp
# via aiosignal
genson==1.3.1
# via datamodel-code-generator
griffe==1.14.0
h11==0.16.0
# via httpcore
httpcore==1.0.9
Expand All @@ -70,35 +81,53 @@ idna==3.11
# via httpx
# via yarl
importlib-metadata==8.7.1
# via isort
# via typeguard
inflect==7.5.0
# via datamodel-code-generator
iniconfig==2.1.0
# via pytest
isort==6.1.0
# via datamodel-code-generator
jinja2==3.1.6
# via datamodel-code-generator
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.3
# via jinja2
mdurl==0.1.2
# via markdown-it-py
more-itertools==10.8.0
# via inflect
multidict==6.7.0
# via aiohttp
# via yarl
mypy==1.17.0
mypy-extensions==1.1.0
# via black
# via mypy
nodeenv==1.10.0
# via pyright
nox==2025.11.12
packaging==25.0
# via black
# via datamodel-code-generator
# via dependency-groups
# via nox
# via pytest
pathspec==1.0.3
# via black
# via mypy
platformdirs==4.4.0
# via black
# via virtualenv
pluggy==1.6.0
# via pytest
propcache==0.4.1
# via aiohttp
# via yarl
pydantic==2.12.5
# via datamodel-code-generator
# via landingai-ade
pydantic-core==2.41.5
# via pydantic
Expand All @@ -113,6 +142,10 @@ pytest-asyncio==1.2.0
pytest-xdist==3.8.0
python-dateutil==2.9.0.post0
# via time-machine
pytokens==0.4.1
# via black
pyyaml==6.0.3
# via datamodel-code-generator
respx==0.22.0
rich==14.2.0
ruff==0.14.13
Expand All @@ -122,13 +155,18 @@ sniffio==1.3.1
# via landingai-ade
time-machine==2.19.0
tomli==2.4.0
# via black
# via datamodel-code-generator
# via dependency-groups
# via mypy
# via nox
# via pytest
typeguard==4.5.2
# via inflect
typing-extensions==4.15.0
# via aiosignal
# via anyio
# via black
# via exceptiongroup
# via landingai-ade
# via multidict
Expand All @@ -137,6 +175,7 @@ typing-extensions==4.15.0
# via pydantic-core
# via pyright
# via pytest-asyncio
# via typeguard
# via typing-inspection
# via virtualenv
typing-inspection==0.4.2
Expand Down
29 changes: 29 additions & 0 deletions scripts/spec-sync/check-drift.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Compare a live spec against its committed snapshot.
# exit 0 -> no drift
# exit 10 -> drift detected; <committed-path> updated in place with the live spec
# other -> operational error (e.g. fetch failure)
set -euo pipefail

if [ "$#" -ne 2 ]; then
echo "usage: check-drift.sh <spec-url> <committed-path>" >&2
exit 2
fi

url="$1"
committed="$2"
here="$(cd "$(dirname "$0")" && pwd)"
tmp="$(mktemp)"
trap 'rm -f "$tmp"' EXIT

"$here/fetch-normalize.sh" "$url" > "$tmp"

if [ -f "$committed" ] && diff -q "$committed" "$tmp" >/dev/null; then
echo "no drift: $committed"
exit 0
fi

mkdir -p "$(dirname "$committed")"
cp "$tmp" "$committed"
Comment thread
Copilot marked this conversation as resolved.
echo "drift detected -> updated $committed"
exit 10
Loading