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
63 changes: 63 additions & 0 deletions .github/workflows/oasdiff-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
name: OpenAPI Diff Comment

# Split from oasdiff.yml specifically so this job — the only one that needs a write-capable
# GITHUB_TOKEN — never checks out, fetches, or executes anything from the PR/fork. It only
# downloads the plain-text report artifact that oasdiff.yml (running with a read-only token,
# on `pull_request`) already produced, and posts/updates it as a PR comment. This is GitHub's
# own documented pattern for safely commenting on fork PRs — see:
# https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
on:
workflow_run:
workflows: ["OpenAPI Diff (oasdiff)"]
types:
- completed

permissions:
actions: read
pull-requests: write

jobs:
comment:
name: Post oasdiff report comment
runs-on: ubuntu-latest
if: github.event.workflow_run.conclusion == 'success'
steps:
- name: Download report artifact
uses: actions/download-artifact@v4
with:
name: oasdiff-report
path: /tmp/oasdiff
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const marker = '<!-- oasdiff-report -->';
const prNumber = parseInt(fs.readFileSync('/tmp/oasdiff/pr_number.txt', 'utf8').trim(), 10);
const body = marker + '\n' + fs.readFileSync('/tmp/oasdiff/report.md', 'utf8');

const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find(c => c.body && c.body.startsWith(marker));

if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}
115 changes: 115 additions & 0 deletions .github/workflows/oasdiff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: OpenAPI Diff (oasdiff)

# Deliberately plain `pull_request` (not `_target`), so this job never gets a write-capable
# GITHUB_TOKEN and never needs one — it only reads spec JSON and writes a report. GitHub always
# gives `pull_request` a read-only token for fork PRs regardless of `permissions:` below, which
# is exactly what we want here: this job has nothing to write. Posting the sticky PR comment is
# the separate, privileged job in .github/workflows/oasdiff-comment.yml (triggered by
# `workflow_run` once this one finishes), which never checks out or executes anything from the
# PR — see that file for why that split exists.
on:
pull_request:
paths:
- 'api/**'
- '.github/workflows/oasdiff.yml'

permissions:
contents: read

jobs:
oasdiff:
name: oasdiff breaking-change report
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Resolve base/head OpenAPI spec files
id: specs
run: |
set -euo pipefail
BASE_SHA="${{ github.event.pull_request.base.sha }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
mkdir -p /tmp/oasdiff

base_spec=$(git ls-tree -r --name-only "$BASE_SHA" -- api | grep -E '^api/graphiant_api_docs_v[0-9.]+\.json$' | sort -V | tail -1 || true)
head_spec=$(git ls-tree -r --name-only "$HEAD_SHA" -- api | grep -E '^api/graphiant_api_docs_v[0-9.]+\.json$' | sort -V | tail -1 || true)

if [ -z "$base_spec" ] || [ -z "$head_spec" ]; then
echo "No versioned OpenAPI spec found on one side of the diff — skipping oasdiff." | tee /tmp/oasdiff/report.md >> "$GITHUB_STEP_SUMMARY"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi

git show "$BASE_SHA:$base_spec" > /tmp/oasdiff/base.json
git show "$HEAD_SHA:$head_spec" > /tmp/oasdiff/head.json

if cmp -s /tmp/oasdiff/base.json /tmp/oasdiff/head.json; then
echo "OpenAPI spec content unchanged — skipping oasdiff." | tee /tmp/oasdiff/report.md >> "$GITHUB_STEP_SUMMARY"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "skip=false" >> "$GITHUB_OUTPUT"
echo "base_spec=$base_spec" >> "$GITHUB_OUTPUT"
echo "head_spec=$head_spec" >> "$GITHUB_OUTPUT"

# Emits ::error:: workflow commands, which GitHub renders as inline
# annotations on the "Files changed" tab and in the job's Checks output.
- name: Annotate breaking changes
if: steps.specs.outputs.skip != 'true'
run: |
docker run --rm --mount type=bind,src=/tmp/oasdiff,dst=/data tufin/oasdiff breaking \
--format githubactions /data/base.json /data/head.json \
| sed 's#/data/head\.json#${{ steps.specs.outputs.head_spec }}#g'

- name: Build oasdiff report
if: steps.specs.outputs.skip != 'true'
run: |
{
echo "## oasdiff: \`${{ steps.specs.outputs.base_spec }}\` → \`${{ steps.specs.outputs.head_spec }}\`"
echo
docker run --rm --mount type=bind,src=/tmp/oasdiff,dst=/data tufin/oasdiff breaking \
--format text /data/base.json /data/head.json > /tmp/oasdiff/breaking.txt || true

# Each breaking-change finding is a line starting with "error" (tab-separated); the
# summary/no-change lines never start a line that way, so this is more robust than
# matching oasdiff's exact wording, which differs between "no changes at all" and
# "changes, but none breaking".
if ! grep -qE '^error\b' /tmp/oasdiff/breaking.txt; then
echo "✅ **No breaking changes** detected by [oasdiff](https://github.com/oasdiff/oasdiff)."
else
echo "⚠️ **oasdiff reports non-backward-compatible changes** in this spec update — review before releasing:"
echo
echo '```'
cat /tmp/oasdiff/breaking.txt
echo '```'
fi

echo
echo "<details><summary>Full spec changelog (oasdiff)</summary>"
echo
docker run --rm --mount type=bind,src=/tmp/oasdiff,dst=/data tufin/oasdiff changelog \
--format markdown /data/base.json /data/head.json || true
echo
echo "</details>"
echo
echo "_Run \`bash scripts/oasdiff.sh\` locally to reproduce, or \`make oasdiff-changelog\` for the full changelog (requires Docker)._"
} | tee /tmp/oasdiff/report.md >> "$GITHUB_STEP_SUMMARY"

# Always runs (report.md exists whether or not oasdiff itself ran — the resolve step above
# writes a short placeholder report in the skip case) so oasdiff-comment.yml can always
# find an artifact to download, with no special-casing on its end.
- name: Save PR number
run: echo "${{ github.event.pull_request.number }}" > /tmp/oasdiff/pr_number.txt

- name: Upload report artifact
uses: actions/upload-artifact@v4
with:
name: oasdiff-report
path: |
/tmp/oasdiff/report.md
/tmp/oasdiff/pr_number.txt
retention-days: 1
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,6 @@ cython_debug/

.DS_Store
.openapi-generator
git_push.sh
git_push.sh
# oasdiff local scratch file
api/.oasdiff-base.json
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

## [26.8.0] - 2026-08-26

> **⚠️ Breaking change in the underlying API** (flagged by [oasdiff](https://github.com/oasdiff/oasdiff); not a SemVer-major bump since this API doesn't follow strict SemVer): the Zendesk integration endpoints (`POST /v2/integration/`, `PUT /v2/integration/{integrationId}`, `GET /v2/integration/getall/{enterpriseId}`) now **require** `zendeskClientId`/`zendeskClientSecret` in the request body in place of `zendeskApiToken`/`zendeskEmail`. See `AlertserviceZendeskDetails` below.

### Added
- **API spec:** `graphiant_api_docs_v26.8.0.json`
- **Adaptive FEC:** **`PUT /v1/adaptive-fec`** — update adaptive FEC settings for an enterprise; models **`V1AdaptiveFecPutRequest`**, **`ManaV2AdaptiveFecConfiguration`** (`adaptive_fec_enabled`, `sla_classes`)
- **`V1EnterpriseConfigurationGetResponse`**: added `adaptive_fec_config` field (**`ManaV2AdaptiveFecConfiguration`**)
- **`AlertserviceZendeskDetails`**: added `zendesk_client_id` and `zendesk_client_secret` fields (Zendesk OAuth credentials) — now **required** in place of the deprecated `zendesk_api_token`/`zendesk_email`
- **`GET /v1/extranet/b2b/services/summary`**: added optional `serviceType` query parameter
- **CI/tooling:** `OpenAPI Diff (oasdiff)` workflow — [oasdiff](https://github.com/oasdiff/oasdiff) breaking-change report on any PR touching `api/graphiant_api_docs_*.json`; `make oasdiff` / `make oasdiff-changelog` reproduce it locally

### Changed
- **Version:** Package **26.8.0**; OpenAPI bundle **`graphiant_api_docs_v26.8.0.json`** (replaces **26.7.0** bundle in-repo).
- **`AlertserviceZendeskDetails`**: `zendesk_api_token` and `zendesk_email` are now optional in the schema (deprecated in favor of `zendesk_client_secret`/`zendesk_client_id`), but the underlying `/v2/integration/*` endpoints now require the new OAuth fields — see breaking-change note above.
- **SDK (generated):** Refreshed **`default_api`**, model exports, and **`docs/`** to match the **26.8.0** spec.

### Removed
- **API spec:** removed **`graphiant_api_docs_v26.7.0.json`** (superseded by **26.8.0** bundle).

## [26.7.0] - 2026-07-27

### Added
Expand Down
20 changes: 20 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,19 @@ make generate

Review `git diff` carefully after generation — pay particular attention to files in `.openapi-generator-ignore` to confirm they were not overwritten.

### Checking for breaking API changes (oasdiff)

Any PR that touches `api/graphiant_api_docs_*.json` automatically gets an [oasdiff](https://github.com/oasdiff/oasdiff) breaking-change report — a job summary and inline annotations on the spec file from `OpenAPI Diff (oasdiff)` (`.github/workflows/oasdiff.yml`), plus a sticky PR comment (updated in place on each push, not duplicated) from a follow-up job, `OpenAPI Diff Comment` (`.github/workflows/oasdiff-comment.yml`). That job is split out and triggered via `workflow_run` specifically so it can safely get a write-capable token for PRs from forks, without ever needing to check out or execute anything from the PR itself — see the comments in that file for the reasoning. No need to run any of this manually before opening a PR, but you can reproduce it locally while drafting the `CHANGELOG.md` entry:

```bash
make oasdiff # breaking-change report
make oasdiff-changelog # full markdown changelog of every spec change
```

Requires Docker (pulls `tufin/oasdiff`). By default it diffs the two highest-versioned specs found in `api/` if you've dropped a new one in alongside the old one, otherwise the latest local spec against the version committed on `origin/main`.

The CI check is informational, not a merge gate — the Graphiant API doesn't follow strict semver, so a spec bump can legitimately include breaking changes. If oasdiff flags something, call it out explicitly in the PR description and `CHANGELOG.md` entry rather than silently letting the generated SDK diff speak for itself.

## Code Standards

### Python Code
Expand Down Expand Up @@ -300,6 +313,13 @@ If tests fail:
2. Run specific test: `pytest tests/test_file.py::test_function -v`
3. Check test coverage: `pytest --cov=graphiant_sdk --cov-report=term tests/`

### Unexpected model changes after `make generate`

`openapi-generator` deduplicates inline anonymous response schemas (e.g. simple `{"error": "string"}` error bodies reused across several endpoints) by content hash, and which model name "wins" the dedup is not stable across runs — even with an unchanged spec and the same generator version. If `git diff` after regenerating shows response model classes disappearing from `graphiant_sdk/__init__.py` / `graphiant_sdk/models/__init__.py` or being renamed in `default_api.py`'s `_response_types_map`, for endpoints your spec change didn't touch:

1. Confirm it's not a real spec change: diff the specific path's `responses` block between old and new spec JSON directly.
2. If unchanged, it's generator nondeterminism — restore the class names/exports that are on `main` today rather than let them silently drop from the SDK's public API as a side effect of an unrelated spec update.

### Merge Conflicts

If you have merge conflicts:
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: install test lint type-check build generate clean help
.PHONY: install test lint type-check build generate oasdiff oasdiff-changelog clean help

## install: Install package in editable mode with dev dependencies
install:
Expand Down Expand Up @@ -30,6 +30,14 @@ build:
generate:
@bash scripts/generate.sh

## oasdiff: Check the OpenAPI spec diff for breaking changes (requires Docker)
oasdiff:
@bash scripts/oasdiff.sh

## oasdiff-changelog: Full markdown changelog of OpenAPI spec changes (requires Docker)
oasdiff-changelog:
@MODE=changelog bash scripts/oasdiff.sh

## clean: Remove build artifacts
clean:
rm -rf dist/ build/ *.egg-info/
Expand Down
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ More product and platform context: [Graphiant Docs](https://docs.graphiant.com).
| **Automation** | [Graphiant Automation](https://docs.graphiant.com/docs/automation) |
| **REST API** | [Graphiant Portal REST API](https://docs.graphiant.com/docs/graphiant-portal-rest-api) |
| **Method index (repo)** | [DefaultApi.md](https://github.com/Graphiant-Inc/graphiant-sdk-python/blob/main/docs/DefaultApi.md) |
| **OpenAPI bundle (this build)** | [`api/graphiant_api_docs_v26.7.0.json`](https://github.com/Graphiant-Inc/graphiant-sdk-python/blob/main/api/graphiant_api_docs_v26.7.0.json) — source for generated paths and models |
| **OpenAPI bundle (this build)** | [`api/graphiant_api_docs_v26.8.0.json`](https://github.com/Graphiant-Inc/graphiant-sdk-python/blob/main/api/graphiant_api_docs_v26.8.0.json) — source for generated paths and models |
| **Model docs (`*.md`)** | [`docs/`](https://github.com/Graphiant-Inc/graphiant-sdk-python/tree/main/docs) (same names as Python classes, e.g. `V1EdgesSummaryGetResponse.md`) |
| **PyPI** | [graphiant-sdk](https://pypi.org/project/graphiant-sdk) |
| **Changelog** | [CHANGELOG.md](https://github.com/Graphiant-Inc/graphiant-sdk-python/blob/main/CHANGELOG.md) |
Expand Down Expand Up @@ -462,6 +462,12 @@ make type-check
make build # python -m build
```

Build artifacts land in `dist/` (`graphiant_sdk-<version>.tar.gz` sdist, `graphiant_sdk-<version>-py3-none-any.whl` wheel). To try a build before it's published to PyPI — e.g. in another project or a test-infrastructure repo — install directly from the file:

```bash
pip install /path/to/graphiant-sdk-python/dist/graphiant_sdk-26.8.0.tar.gz
```

Or without `make`:

```bash
Expand All @@ -477,12 +483,26 @@ To regenerate the SDK from the latest API specification:
make generate

# Or run the script directly (supports OPENAPI_SPEC override)
OPENAPI_SPEC=api/graphiant_api_docs_v26.7.0.json bash scripts/generate.sh
OPENAPI_SPEC=api/graphiant_api_docs_v26.8.0.json bash scripts/generate.sh
```

`scripts/generate.sh` wraps the full `openapi-generator-cli generate` invocation (requires OpenAPI Generator **>= 7.23.0**). It auto-detects `openapi-generator` (Homebrew) or `openapi-generator-cli` (npm), reads the current version from `pyproject.toml`, and passes `--git-user-id`/`--git-repo-id` so generated docs never contain `GIT_USER_ID` placeholders.

> **Note:** Download the latest API bundle from the Graphiant portal under **Support Hub** → **Developer Tools** and place it in `api/`. The versioned JSON bundle (`api/graphiant_api_docs_v26.7.0.json`) is the snapshot used for this release; `api/openapi.yaml` is the primary YAML spec. Hand-written files listed in `.openapi-generator-ignore` (`graphiant_cli/`, `graphiant_sdk/api_client.py`, `configuration.py`, etc.) are never overwritten by the generator.
> **Note:** Download the latest API bundle from the Graphiant portal under **Support Hub** → **Developer Tools** and place it in `api/`. The versioned JSON bundle (`api/graphiant_api_docs_v26.8.0.json`) is the snapshot used for this release; `api/openapi.yaml` is the primary YAML spec. Hand-written files listed in `.openapi-generator-ignore` (`graphiant_cli/`, `graphiant_sdk/api_client.py`, `configuration.py`, etc.) are never overwritten by the generator.

#### Checking for breaking API changes (oasdiff)

Any PR touching `api/graphiant_api_docs_*.json` gets an automated [oasdiff](https://github.com/oasdiff/oasdiff) breaking-change report from the `OpenAPI Diff (oasdiff)` GitHub Actions workflow — a job summary, inline annotations, and a sticky PR comment (updated in place on each push). To reproduce it locally (requires Docker) — handy for figuring out what to write in the `CHANGELOG.md` entry for a spec update:

```bash
make oasdiff # breaking-change report
make oasdiff-changelog # full markdown changelog of every spec change

# Or against explicit files:
bash scripts/oasdiff.sh api/graphiant_api_docs_v26.7.0.json api/graphiant_api_docs_v26.8.0.json
```

The check is informational, not a merge gate — this API doesn't follow strict SemVer, so a spec bump can legitimately include breaking changes. See `CONTRIBUTING.md` for details.

### Testing

Expand All @@ -498,7 +518,7 @@ pytest tests/ --cov=graphiant_sdk --cov-report=html

### Source of truth (this release)

Operations and schemas are generated from **`api/graphiant_api_docs_v26.7.0.json`** (in `api/` and bundled in the PyPI wheel). For a newer portal/API, download the current bundle (Support Hub → Developer Tools) and diff paths before relying on URLs here.
Operations and schemas are generated from **`api/graphiant_api_docs_v26.8.0.json`** (in `api/` and bundled in the PyPI wheel). For a newer portal/API, download the current bundle (Support Hub → Developer Tools) and diff paths before relying on URLs here.

| How to explore | Where |
|----------------|-------|
Expand Down Expand Up @@ -529,7 +549,7 @@ REST query parameters use **camelCase** in URLs (`enterpriseId`). Generated Pyth
| **`V1GlobalSummaryPostRequest`**, **`V1GlobalSummaryPostResponse`** | **`POST /v1/global/summary`** |
| **`V2ParentalertlistPostRequest`**, **`V2ParentalertlistPostResponse`** | **`POST /v2/parentalertlist`** |

### Sample HTTP endpoints (from `graphiant_api_docs_v26.7.0.json`)
### Sample HTTP endpoints (from `graphiant_api_docs_v26.8.0.json`)

The API surface is large; this table lists **real** paths from the bundled spec. For the full set, use **`graphiant api list`** or **`DefaultApi.md`**.

Expand Down
Loading
Loading