Skip to content
Merged
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
85 changes: 85 additions & 0 deletions .github/workflows/e2e-production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# V1 end-to-end tests against the LIVE PRODUCTION API (api.va.landing.ai).
#
# Unlike the staging contract gate in pr-gates.yml, this is NOT wired to pull requests.
# It runs in exactly two places:
# 1. manually — Actions -> "E2E Production (V1)" -> Run workflow;
# 2. as the pre-tag gate in release.yml, which calls this workflow via `workflow_call`
# and refuses to stamp/commit/tag if it goes red.
#
# COST: every run exercises the full V1 surface (parse, extract, build-schema, classify,
# section, split, and both job types) against production, so it spends real inference
# credits and leaves real job records in the production org — roughly 8 inference calls
# on a 2-page PDF. That is the deliberate price of gating releases on the real API; do
# not add this to a per-PR trigger.
name: E2E Production (V1)

on:
workflow_dispatch:
workflow_call:

# Serialize runs so a manual dispatch cannot interleave with the gate of an in-flight
# release (both hit the same production org). A second run queues rather than racing.
concurrency:
group: e2e-production
cancel-in-progress: false

jobs:
e2e-production:
name: e2e (production V1)
runs-on: ubuntu-latest
timeout-minutes: 30
# Defense in depth: only main should reach the production key. release.yml runs on main
# and a manual dispatch is expected on main; anything else skips here. NOTE this is not
# the real control — a workflow_dispatch runs the SELECTED ref's copy of this file, which
# could drop the `if`, so the environment branch rule below is what actually protects the
# secret. A non-main dispatch now skips silently rather than failing; that's expected.
if: github.ref == 'refs/heads/main'
# Scopes the production key to a GitHub Environment instead of exposing it to every
# workflow in the repo. REQUIRED SETUP: in repo Settings -> Environments, create
# `production-e2e`, add LANDINGAI_ADE_PRODUCTION_APIKEY as an environment secret, and
# under "Deployment branches and tags" restrict it to `main` only. That branch rule is
# the control that actually protects the key: GitHub enforces it against the run's real
# ref, so a workflow_dispatch on another branch cannot hand its (possibly modified)
# checkout the production credential. The release gate still works — release.yml runs on
# main and calls this workflow, which inherits the caller's main ref. Keep the secret
# environment-only: do NOT also store it as a repo-level secret (every branch can read
# that, defeating the branch rule). Do NOT add required reviewers unless you want every
# release to pause for a manual approval — release.yml blocks on this job.
environment: production-e2e
Comment thread
tian-lan-landing marked this conversation as resolved.
steps:
- uses: actions/checkout@v6

# The tests self-skip when the key is absent (so a local `pytest -m production`
# is harmless). In CI that would turn a missing or rotated secret into a green
# run with zero tests executed — and, via release.yml, a release gated on
# nothing. Fail loudly here instead.
- name: Require the production API key
env:
LANDINGAI_ADE_PRODUCTION_APIKEY: ${{ secrets.LANDINGAI_ADE_PRODUCTION_APIKEY }}
run: |
if [ -z "$LANDINGAI_ADE_PRODUCTION_APIKEY" ]; then
echo "::error::LANDINGAI_ADE_PRODUCTION_APIKEY is not set. Add it as a secret on the" \
"\`production-e2e\` environment (or at the repo level). Refusing to report a green" \
"production e2e run with no tests executed."
exit 1
fi

- 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: V1 e2e vs production
env:
LANDINGAI_ADE_PRODUCTION_APIKEY: ${{ secrets.LANDINGAI_ADE_PRODUCTION_APIKEY }}
# `-m production` overrides the `-m 'not contract and not production'` default in
# pyproject.toml. -n 0 forces serial execution: these hit the live API, and the
# repo's default `-n auto` (xdist) would run them in parallel, inviting flakiness
# and rate-limits — and would re-parse the sample document once per worker.
run: rye run pytest tests/contract -m production -n 0 -v
20 changes: 20 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#
# A maintainer runs this workflow (Actions -> Release -> Run workflow) and
# picks the bump: patch / minor / major. The workflow then, in one shot:
# 0. runs the V1 production e2e suite (.github/workflows/e2e-production.yml)
# against the live API — nothing below runs if it goes red,
# 1. computes the next version from the latest git tag,
# 2. stamps it into pyproject.toml and src/landingai_ade/_version.py,
# 3. prepends a changelog section (grouped by Conventional Commit prefixes)
Expand All @@ -13,6 +15,12 @@
# 6. tags it and creates the GitHub Release, which triggers
# .github/workflows/publish-pypi.yml.
#
# Both gates (0 and 4) sit before any commit, tag, or GitHub Release exists, so
# an aborted release leaves no trace to clean up. Note the trade-off in gate 0:
# a production API outage blocks releasing. That is intentional — shipping an
# SDK whose live surface is broken is worse — but it does mean an urgent release
# during an incident needs gate 0 temporarily removed from this file.
#
# No release PR, no second confirmation: dispatching the workflow IS the
# release decision. Ordinary PR merges never trigger any of this.
#
Expand Down Expand Up @@ -45,9 +53,21 @@ concurrency:
cancel-in-progress: false

jobs:
# Gate the release on the live production API before anything is stamped or tagged.
# `secrets: inherit` hands the called workflow LANDINGAI_ADE_PRODUCTION_APIKEY; it
# fails fast if that secret is missing rather than passing with zero tests run.
e2e-production:
name: e2e gate
if: github.repository == 'landing-ai/ade-python' && github.ref == 'refs/heads/main'
uses: ./.github/workflows/e2e-production.yml
secrets: inherit

release:
name: release
runs-on: ubuntu-latest
# A skipped or failed gate skips the release too, so there is no path to a tag
# that did not pass production e2e.
needs: e2e-production
if: github.repository == 'landing-ai/ade-python' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v6
Expand Down
11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,15 +134,18 @@ replacement = '[\1](https://github.com/landing-ai/ade-python/tree/main/\g<2>)'

[tool.pytest.ini_options]
testpaths = ["tests"]
# Contract tests (marker `contract`) hit the live staging API — opt-in only, so a plain
# `rye run pytest` (or the pydantic-v1 nox session) never silently calls staging even when
# LANDINGAI_ADE_STAGING_APIKEY is exported. CI's explicit `-m contract` overrides this.
addopts = "--tb=short -n auto -m 'not contract'"
# Live-API tests are opt-in only, so a plain `rye run pytest` (or the pydantic-v1 nox session)
# never silently calls a real environment even when the keys are exported. CI's explicit
# `-m contract` / `-m production` overrides this.
# contract -> staging (pr-gates.yml, spec-sync PRs)
# production -> PRODUCTION, spends real credits (e2e-production.yml, dispatch + release gate)
addopts = "--tb=short -n auto -m 'not contract and not production'"
xfail_strict = true
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "session"
markers = [
"contract: hits the live staging API; requires LANDINGAI_ADE_STAGING_APIKEY",
"production: hits the live PRODUCTION API and spends real credits; requires LANDINGAI_ADE_PRODUCTION_APIKEY",
]
filterwarnings = [
"error"
Expand Down
Loading