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
134 changes: 134 additions & 0 deletions .github/workflows/wire-shape-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: Wire-Shape Contract

# QF-14 Java arm: blocks drift between Java @JsonProperty-annotated
# classes and the OpenAPI specs that are the authoritative wire
# contract. Runs on every PR and push to main. Drift NOT covered by
# the baseline fails the check.
#
# Specs are fetched from the getaxonflow/axonflow community mirror at
# the SHA recorded in tests/fixtures/wire-shape-baseline.json so the
# gate is deterministic. A 'spec-pin-bump' label is required on PRs
# that change the SHA, preserving review integrity (a PR that both
# moved the SHA and the Java classes could otherwise silence drift).
#
# To regenerate the baseline:
# python3 scripts/wire_shape/refresh.py <specs_dir>

on:
pull_request:
branches: [main]
paths:
- 'src/main/java/**/*.java'
- 'tests/fixtures/wire-shape-baseline.json'
- 'scripts/wire_shape/**'
- '.github/workflows/wire-shape-contract.yml'
push:
branches: [main]
paths:
- 'src/main/java/**/*.java'
- 'tests/fixtures/wire-shape-baseline.json'
- 'scripts/wire_shape/**'
- '.github/workflows/wire-shape-contract.yml'

permissions:
contents: read

jobs:
wire-shape:
name: Validate Wire Shape
runs-on: ubuntu-latest
env:
DO_NOT_TRACK: '1'
steps:
- name: Checkout SDK (full history for SHA-bump guard)
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Read pinned OpenAPI specs SHA from baseline
id: specs_sha
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
import json
import sys
path = "tests/fixtures/wire-shape-baseline.json"
data = json.load(open(path))
sha = (data.get("openapi_specs_sha", "") or "").strip()
if not sha:
print(
f"::error::{path} is missing openapi_specs_sha. "
"Regenerate via scripts/wire_shape/refresh.py.",
file=sys.stderr,
)
sys.exit(1)
print(f"sha={sha}")
PY

- name: Guard against unauthorized OpenAPI specs SHA bump
if: github.event_name == 'pull_request'
env:
PR_LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
BASE_REF: ${{ github.base_ref }}
PR_SHA: ${{ steps.specs_sha.outputs.sha }}
run: |
set -e
BASE_FILE=$(mktemp)
if git show "origin/${BASE_REF}:tests/fixtures/wire-shape-baseline.json" > "$BASE_FILE" 2>/dev/null; then
# File exists on the base branch. It MUST parse — a malformed
# baseline file would otherwise let `BASE_SHA` come back empty
# and route this PR through the "first pin introduction"
# bypass below, silently authorizing a SHA bump.
BASE_SHA=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1])).get('openapi_specs_sha','') or '')" "$BASE_FILE")
else
# File genuinely absent on the base branch (first-time
# introduction). Distinguishing this from "file present but
# unparseable" is what guards the bypass.
BASE_SHA=""
fi
rm -f "$BASE_FILE"
if [ -z "$BASE_SHA" ]; then
if git cat-file -e "origin/${BASE_REF}:tests/fixtures/wire-shape-baseline.json" 2>/dev/null; then
echo "::error::tests/fixtures/wire-shape-baseline.json on origin/${BASE_REF} parsed with empty openapi_specs_sha."
echo "::error::This is unrecoverable from inside the gate. Re-run scripts/wire_shape/refresh.py on main."
exit 1
fi
echo "::notice::Base branch has no wire-shape-baseline.json yet; treating this PR as first pin introduction."
exit 0
fi
if [ "$BASE_SHA" = "$PR_SHA" ]; then
echo "openapi_specs_sha unchanged (${PR_SHA})."
exit 0
fi
echo "SHA change detected: ${BASE_SHA} -> ${PR_SHA}"
HAS_LABEL=$(printf '%s' "$PR_LABELS" | python3 -c "import json, sys; print('spec-pin-bump' in json.load(sys.stdin))")
if [ "$HAS_LABEL" = "True" ]; then
echo "::notice::'spec-pin-bump' label present — SHA bump authorized."
exit 0
fi
echo "::error::openapi_specs_sha changed from ${BASE_SHA} to ${PR_SHA}."
echo "::error::The wire-shape contract's spec revision is pinned to preserve"
echo "::error::review integrity: a SHA change in the same PR as SDK changes"
echo "::error::can silence drift by retargeting the contract to a friendlier"
echo "::error::revision. Either split into a dedicated SHA-bump PR, or"
echo "::error::apply the 'spec-pin-bump' label to this PR."
exit 1

- name: Checkout OpenAPI specs (pinned to baseline SHA)
uses: actions/checkout@v4
with:
repository: getaxonflow/axonflow
ref: ${{ steps.specs_sha.outputs.sha }}
path: axonflow-community

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install PyYAML
run: pip install 'pyyaml>=6,<7'

- name: Run wire-shape contract validator
env:
AXONFLOW_OPENAPI_SPECS_DIR: ${{ github.workspace }}/axonflow-community/docs/api
run: python3 scripts/wire_shape/validate.py
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- **Version alignment check** (`.github/workflows/validate-version-alignment.yml`). CI now fails any PR or push to `main` where `pom.xml`'s `<version>` drifts from the first released `## [X.Y.Z]` section in `CHANGELOG.md`. Matches the pattern in the platform repo and the Go SDK.
- **Wire-shape contract gate** (`.github/workflows/wire-shape-contract.yml`). CI fails any PR that introduces drift between Java `@JsonProperty` annotations and the OpenAPI specs pinned at `tests/fixtures/wire-shape-baseline.json::openapi_specs_sha`. Four gates: cross-spec schema divergence, intra-file schema duplicates, per-type SDK-vs-spec drift, and registered-type rename-escape. The pinned spec SHA is itself guarded by a `spec-pin-bump` PR label so a single PR can't both move the SHA and silence drift. Source-discovery walks brace depth so nested classes (e.g. `WorkflowTypes.CreateWorkflowRequest`) and inner enums are attributed to the correct type rather than the file's outer class. Mirrors the Python, Go, and TypeScript gates.
- **`WebhookSubscription.secret`** — HMAC-SHA256 signing key now exposed on the response from `createWebhook`. Required to verify the `X-AxonFlow-Signature` header on inbound webhook deliveries; without it, callers can't validate payload authenticity. Also adds `tenantId` and `orgId` (ownership scoping). The 6-arg constructor is preserved as a source-compat overload that delegates to the 9-arg with nulls for the new fields. `toString()` redacts `secret` to avoid log leakage.
- **`BudgetAlert.acknowledged`** — alert dismissal flag. Also adds `@JsonProperty` annotations on previously-unannotated fields (`id`, `threshold`, `message`) so the wire-shape gate can see them; Jackson's default name mapping was correct, but the validator's discovery walks `@JsonProperty` only.

### Changed

- **`WebhookSubscription` equality is now identity-based on `id`.** A `WebhookSubscription` is an entity, not a value object — two instances with the same `id` represent the same subscription, regardless of whether one view has loaded `secret` (returned by `createWebhook` only) and another has not, or whether `updatedAt`/`active` have moved between fetches. Previously `equals()`/`hashCode()` compared every field; that meant a webhook constructed locally with the legacy 6-arg constructor would have compared **unequal** to the same logical webhook deserialized from a server response that included `secret`/`tenantId`/`orgId`. `Set<WebhookSubscription>`, `Map` keying, and identity-tracking caches all break under that semantics. Identity-based equality fixes those at the source. If you need content-equality (e.g. to detect a rotated secret), compare the relevant getters directly. Same change applies to `hashCode()`. `toString()` is unchanged (still includes the full state with `secret` redacted).

## [5.7.0] - 2026-04-22

Expand Down
Loading
Loading