diff --git a/.github/workflows/api-diff.yml b/.github/workflows/api-diff.yml new file mode 100644 index 000000000..0bfca4802 --- /dev/null +++ b/.github/workflows/api-diff.yml @@ -0,0 +1,69 @@ +name: OpenAPI Spec Diff Check + +on: + pull_request: + push: + branches: + - master + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run branch logic unit tests + run: ./scripts/api-diff/api-diff.test.sh + + api-diff: + runs-on: ubuntu-latest + needs: test + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Make script executable + run: chmod +x scripts/api-diff/api-diff.sh + + - name: Run API diff check + run: ./scripts/api-diff/api-diff.sh + + html-reports: + runs-on: ubuntu-latest + needs: test + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Make script executable + run: chmod +x scripts/api-diff/api-diff.sh + + - name: Generate HTML reports + run: ./scripts/api-diff/api-diff.sh --html-report + + - name: Upload HTML reports + uses: actions/upload-artifact@v4 + if: always() + with: + name: openapi-diff-reports + path: reports/ + retention-days: 30 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 16dcdb1fa..6ed2b29fd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ node_modules # JetBrains generated files -.idea \ No newline at end of file +.idea + +# Temporary files for API diff checks +tmp/ \ No newline at end of file diff --git a/README.md b/README.md index 780ed30a7..801f84655 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,26 @@ Once you sign up or login, you can create a new API under your account and impor ## Updates If you find something missing or incorrect please [open an issue](https://github.com/XeroAPI/Xero-OpenAPI/issues/new) or send us a pull request. +## API Diff Checking +This repository includes automated API diff checking using [openapi-diff](https://github.com/OpenAPITools/openapi-diff) to detect breaking changes and modifications to the OpenAPI specifications. + +### Quick Start +```bash +# Check all xero*.yaml files against master branch +./scripts/api-diff/api-diff.sh + +# Check a single file +./scripts/api-diff/api-diff.sh xero_accounting.yaml +``` + +### Branch Naming Convention +Branches containing `breaking` anywhere in the name will allow breaking changes without failing the build. All other branches will fail if breaking changes are detected. + +**Examples:** `breaking-api-v2`, `feature-breaking-change`, `api-breaking-update` + +### Full Documentation +For detailed usage, configuration options, environment variables, and integration details, see [scripts/api-diff/README.md](scripts/api-diff/README.md). + ## License This software is published under the [MIT License](http://en.wikipedia.org/wiki/MIT_License). diff --git a/scripts/api-diff/README.md b/scripts/api-diff/README.md new file mode 100644 index 000000000..31e22113e --- /dev/null +++ b/scripts/api-diff/README.md @@ -0,0 +1,98 @@ +# API Diff Scripts + +This directory contains scripts for detecting and reporting API changes using [openapi-changes](https://pb33f.io/openapi-changes/). + +## Files + +### `api-diff.sh` +Main script that compares OpenAPI specifications against the master branch. + +**Usage:** +```bash +# From the repo root +./scripts/api-diff/api-diff.sh [--fail-on-breaking] [--html-report] [filename.yaml] + +# Check all xero*.yaml files +./scripts/api-diff/api-diff.sh + +# Check a single file +./scripts/api-diff/api-diff.sh xero_accounting.yaml + +# Fail on breaking changes (CI mode) +./scripts/api-diff/api-diff.sh --fail-on-breaking + +# Generate HTML reports +./scripts/api-diff/api-diff.sh --html-report +``` + +**Environment Variables:** +- `OPENAPI_CHANGES_DOCKER_IMAGE` - Docker image to use (default: `pb33f/openapi-changes:latest`) +- `BASE_BRANCH` - Branch to compare against (default: `origin/master`) + +### Comparing Different Branches + +By default, `api-diff.sh` compares your current branch against `origin/master`. You can compare against any other branch by setting the `BASE_BRANCH` environment variable: + +**Compare current branch against a different target branch:** +```bash +# Compare against origin/develop +BASE_BRANCH=origin/develop ./scripts/api-diff/api-diff.sh + +# Compare against origin/main +BASE_BRANCH=origin/main ./scripts/api-diff/api-diff.sh + +# Compare against a feature branch +BASE_BRANCH=origin/feature/new-api ./scripts/api-diff/api-diff.sh + +# Compare specific file against different branch +BASE_BRANCH=origin/v2-api ./scripts/api-diff/api-diff.sh xero-webhooks.yaml +``` + +**Compare two specific branches (advanced usage):** +If you need to compare two arbitrary branches, you can temporarily switch to one branch and set BASE_BRANCH to the other: + +```bash +# Compare feature-branch against develop +git checkout feature-branch +BASE_BRANCH=origin/develop ./scripts/api-diff/api-diff.sh + +# Compare main against a tag +git checkout main +BASE_BRANCH=v1.0.0 ./scripts/api-diff/api-diff.sh +``` + +### `api-diff.test.sh` +Unit tests for the branch logic pattern matching used in GitHub Actions. + +**Usage:** +```bash +./scripts/api-diff/api-diff.test.sh +``` + +Tests validate that: +- Branches containing `breaking` anywhere in the name are correctly identified +- Other branches are handled with breaking change enforcement +- All command-line flags (`--fail-on-breaking`, `--allow-breaking`, `--dry-run`, `--html-report`) are parsed correctly +- Flag precedence and override behavior works as expected + +## Integration + +These scripts are integrated into the GitHub Actions workflow at `.github/workflows/api-diff.yml`: +- **test-branch-logic** job - Runs unit tests +- **api-diff** job - Runs API diff checks with conditional breaking change enforcement +- **html-reports** job - Generates HTML reports and uploads them as artifacts + +### Branch Naming Convention +The GitHub Actions workflow automatically adjusts its behavior based on branch names: + +**Allow Breaking Changes:** +- Any branch containing `breaking` in the name +- Examples: `breaking-api-v2`, `feature-breaking-change`, `api-breaking-update` +- The `--fail-on-breaking` flag is NOT passed to the script + +**Fail on Breaking Changes:** +- All other branches (main, master, develop, feature branches, etc.) +- The `--fail-on-breaking` flag IS passed to the script +- Build will fail if breaking changes are detected + +This allows developers to explicitly signal when they're working on breaking changes by including `breaking` in their branch name. diff --git a/scripts/api-diff/api-diff.sh b/scripts/api-diff/api-diff.sh new file mode 100755 index 000000000..0fcf94b13 --- /dev/null +++ b/scripts/api-diff/api-diff.sh @@ -0,0 +1,209 @@ +#!/bin/bash + +# Script to check API diffs using openapi-changes +# Usage: ./scripts/api-diff/api-diff.sh [--fail-on-breaking] [--html-report] [filename.yaml] +# Assumes you have Docker installed and the repo is checked out with master branch available + +set -e # Exit on error +set -o pipefail # Catch errors in pipes + +# Change to repo root +cd "$(dirname "$0")/../.." + +# Configuration +DOCKER_IMAGE="${OPENAPI_CHANGES_DOCKER_IMAGE:-pb33f/openapi-changes:latest}" +BASE_BRANCH="${BASE_BRANCH:-origin/master}" + +FAIL_ON_BREAKING=false +TARGET_FILE="" +DRY_RUN=false +HTML_REPORT=false + +# Parse arguments +for arg in "$@"; do + if [ "$arg" = "--fail-on-breaking" ]; then + FAIL_ON_BREAKING=true + elif [ "$arg" = "--dry-run" ]; then + DRY_RUN=true + elif [ "$arg" = "--html-report" ]; then + HTML_REPORT=true + elif [[ "$arg" == *.yaml ]]; then + TARGET_FILE="$arg" + fi +done + +# If --fail-on-breaking not explicitly set, determine based on branch name +if [ "$FAIL_ON_BREAKING" = false ]; then + CURRENT_BRANCH=${CURRENT_BRANCH:-$(git branch --show-current 2>/dev/null || echo "")} + if [[ "$CURRENT_BRANCH" == *breaking* ]]; then + echo "Branch '$CURRENT_BRANCH' contains 'breaking', allowing breaking changes" + FAIL_ON_BREAKING=false + else + echo "Branch '$CURRENT_BRANCH' does not contain 'breaking', failing on breaking changes" + FAIL_ON_BREAKING=true + fi +fi + +if [ "$DRY_RUN" = true ]; then + if [ "$FAIL_ON_BREAKING" = true ]; then + echo "Mode: Failing on breaking changes" + else + echo "Mode: Allowing breaking changes" + fi + echo "Dry run mode, exiting after branch check" + exit 0 +fi + +echo "Starting API diff check..." + +# Ensure we're in the repo root +if [ ! -f "xero_accounting.yaml" ]; then + echo "Error: Not in repo root or xero_accounting.yaml not found" + exit 1 +fi + +# Fetch master if not already done +git fetch "${BASE_BRANCH%%/*}" "${BASE_BRANCH##*/}" 2>/dev/null || echo "Warning: Could not fetch ${BASE_BRANCH}" + +# Use tmp directory for master branch files (outside repo to avoid overlap with /current mount) +TEMP_DIR="./tmp" + +REPORTS_DIR="./reports" + +# Ensure tmp directory exists +rm -rf "$TEMP_DIR" +mkdir -p "$TEMP_DIR" + +# Get list of xero*.yaml files (excluding any master_*.yaml files) +if [ -n "$TARGET_FILE" ]; then + # Single file specified + if [ ! -f "$TARGET_FILE" ]; then + echo "Error: File '$TARGET_FILE' not found" + exit 1 + fi + files="$TARGET_FILE" + echo "Running diff for single file: $TARGET_FILE" +else + # All xero*.yaml files + files=$(ls xero*.yaml 2>/dev/null) + if [ -z "$files" ]; then + echo "No xero*.yaml files found" + exit 1 + fi +fi + +BREAKING_CHANGES_FOUND=false +FILES_WITH_BREAKING_CHANGES=() +TOTAL_FILES=0 +PROCESSED_FILES=0 + +echo "========================================" +echo "API Diff Summary" +echo "Using Docker image: $DOCKER_IMAGE" +echo "Base branch: $BASE_BRANCH" +echo "========================================" + +# Ensure reports directory exists (only once, not per file) +if [ "$HTML_REPORT" = true ]; then + rm -rf "$REPORTS_DIR" + mkdir -p "$REPORTS_DIR" +fi + +for file in $files; do + TOTAL_FILES=$((TOTAL_FILES + 1)) + echo "" + echo "========== $file ==========" + + # Get the file from master branch + if ! git show "$BASE_BRANCH:$file" > "$TEMP_DIR/$file" 2>/dev/null; then + echo "ℹ️ New file (does not exist in master branch)" + continue + fi + + # Verify the temp file was created + if [ ! -f "$TEMP_DIR/$file" ]; then + echo "❌ Failed to create temp file" + continue + fi + + # Fix malformed YAML in base files (temporary workaround for xero-webhooks.yaml) + if [ "$file" = "xero-webhooks.yaml" ]; then + # Skip xero-webhooks.yaml for now + echo "⚠ Skipping $file" + continue + fi + + # Run openapi-changes + if [ "$HTML_REPORT" = true ]; then + echo "--- Generating HTML Report ---" + + REPORT_FILE="$REPORTS_DIR/${file%.yaml}-diff.html" + set +e + docker run --rm -v "$(pwd)":/current -v "$TEMP_DIR":/base -v "$(pwd)/$REPORTS_DIR":/reports "$DOCKER_IMAGE" html-report --no-logo --no-color --report-file /reports/"${file%.yaml}-diff.html" /base/"$file" /current/"$file" 2>&1 + DOCKER_EXIT=$? + set -e + + if [ $DOCKER_EXIT -eq 0 ]; then + echo "✓ HTML report generated: $REPORT_FILE" + else + echo "⚠ Failed to generate HTML report (exit code: $DOCKER_EXIT)" + fi + else + echo "--- API Diff ---" + set +e + DIFF_OUTPUT=$(docker run --rm -v "$(pwd)":/current -v "$TEMP_DIR":/base "$DOCKER_IMAGE" summary --markdown --no-logo --no-color /base/"$file" /current/"$file" 2>&1) + DIFF_EXIT=$? + set -e + + echo "$DIFF_OUTPUT" + + if [ $DIFF_EXIT -eq 0 ]; then + echo "✓ No breaking changes detected" + else + echo "⚠ Breaking changes detected (exit code: $DIFF_EXIT)" + BREAKING_CHANGES_FOUND=true + FILES_WITH_BREAKING_CHANGES+=("$file") + fi + fi + + PROCESSED_FILES=$((PROCESSED_FILES + 1)) +done + +echo "" +echo "========================================" +echo "API Diff check completed" +echo "Processed: $PROCESSED_FILES/$TOTAL_FILES files" +echo "========================================" + +# Summary +if [ "$HTML_REPORT" = true ]; then + echo "" + echo "📊 HTML reports generated:" + if [ -d "reports" ]; then + ls -la $REPORTS_DIR + else + echo "No reports directory found" + fi +elif [ "$BREAKING_CHANGES_FOUND" = true ]; then + echo "" + echo "❌ Breaking changes detected in the following files:" + for file in "${FILES_WITH_BREAKING_CHANGES[@]}"; do + echo " - $file" + # Output GitHub Actions annotation + if [ -n "$GITHUB_ACTIONS" ]; then + echo "::warning file=${file}::Breaking changes detected in this API spec file" + fi + done + + if [ "$FAIL_ON_BREAKING" = true ] && [ "$HTML_REPORT" = false ]; then + echo "" + echo "Exiting with error due to breaking changes" + exit 1 + else + echo "" + echo "Note: Not failing build (use --fail-on-breaking to fail on breaking changes)" + fi +else + echo "" + echo "✓ No breaking changes detected across all files" +fi \ No newline at end of file diff --git a/scripts/api-diff/api-diff.test.sh b/scripts/api-diff/api-diff.test.sh new file mode 100755 index 000000000..c4b2e2922 --- /dev/null +++ b/scripts/api-diff/api-diff.test.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# Unit test for api-diff.sh branch logic +# Tests the script's branch detection and FAIL_ON_BREAKING setting + +set -e + +echo "=== Unit Test: api-diff.sh Branch Logic ===" +echo + +TESTS_PASSED=0 +TESTS_FAILED=0 + +SCRIPT_PATH="scripts/api-diff/api-diff.sh" + +# Helper function to test script with branch +test_branch() { + local branch_name="$1" + local expected_mode="$2" # "allow" or "fail" + local test_name="$3" + + echo "Testing: $test_name (branch: $branch_name)" + + # Run the script in dry-run mode with CURRENT_BRANCH set + local output + output=$(CURRENT_BRANCH="$branch_name" "$SCRIPT_PATH" --dry-run 2>&1) + + # Check the output for the expected message + if [[ "$expected_mode" == "allow" ]]; then + if echo "$output" | grep -q "Mode: Allowing breaking changes"; then + echo " ✓ PASS: Correctly allows breaking changes" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + echo " ✗ FAIL: Expected to allow breaking changes, but output was:" + echo "$output" + TESTS_FAILED=$((TESTS_FAILED + 1)) + fi + elif [[ "$expected_mode" == "fail" ]]; then + if echo "$output" | grep -q "Mode: Failing on breaking changes"; then + echo " ✓ PASS: Correctly fails on breaking changes" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + echo " ✗ FAIL: Expected to fail on breaking changes, but output was:" + echo "$output" + TESTS_FAILED=$((TESTS_FAILED + 1)) + fi + fi + echo +} + +# Test cases: test_branch "branch-name" "expected-mode" "description" +# expected-mode: "allow" = allows breaking changes, "fail" = fails on breaking + +echo "--- Branches that SHOULD allow breaking changes ---" +test_branch "breaking-api-changes" "allow" "Branch with 'breaking' at start" +test_branch "feature-breaking-change" "allow" "Branch with 'breaking' in middle" +test_branch "fix-breaking-bug" "allow" "Branch with 'breaking' in middle" +test_branch "api-breaking-changes" "allow" "Branch with 'breaking' in middle" +test_branch "update-breaking-endpoint" "allow" "Branch with 'breaking' in middle" + +echo "--- Branches that SHOULD fail on breaking changes ---" +test_branch "feature-new-endpoint" "fail" "Normal feature branch" +test_branch "main" "fail" "Main branch" +test_branch "master" "fail" "Master branch" +test_branch "develop" "fail" "Develop branch" +test_branch "add-openapi-diff-tool" "fail" "Current branch name" +test_branch "fix-api-bug" "fail" "Bug fix branch" +test_branch "feature-v2" "fail" "Version feature branch" + +echo "--- Test override with --fail-on-breaking ---" +# Test that --fail-on-breaking overrides branch logic +echo "Testing override: breaking branch with --fail-on-breaking" +output=$(CURRENT_BRANCH="breaking-test" "$SCRIPT_PATH" --dry-run --fail-on-breaking 2>&1) +if echo "$output" | grep -q "Mode: Failing on breaking changes"; then + echo " ✓ PASS: --fail-on-breaking overrides branch logic" + TESTS_PASSED=$((TESTS_PASSED + 1)) +else + echo " ✗ FAIL: --fail-on-breaking did not override, output:" + echo "$output" + TESTS_FAILED=$((TESTS_FAILED + 1)) +fi +echo + +echo "--- Test --dry-run flag ---" +# Test that --dry-run exits early without doing actual work +echo "Testing: --dry-run flag exits early" +output=$("$SCRIPT_PATH" --dry-run 2>&1) +if echo "$output" | grep -q "Dry run mode, exiting after branch check" && ! echo "$output" | grep -q "Starting API diff check"; then + echo " ✓ PASS: --dry-run exits early without starting diff check" + TESTS_PASSED=$((TESTS_PASSED + 1)) +else + echo " ✗ FAIL: --dry-run did not exit early, output:" + echo "$output" + TESTS_FAILED=$((TESTS_FAILED + 1)) +fi +echo + +echo "--- Test --html-report flag parsing ---" +# Test that --html-report flag is parsed correctly (basic parsing test) +echo "Testing: --html-report flag parsing" +# We'll test this by checking that the script doesn't immediately fail with unknown argument +# and that it would proceed to the repo root check (which will fail since we're not in repo root) +output=$("$SCRIPT_PATH" --html-report --dry-run 2>&1) +if echo "$output" | grep -q "Dry run mode, exiting after branch check"; then + echo " ✓ PASS: --html-report flag parsed correctly" + TESTS_PASSED=$((TESTS_PASSED + 1)) +else + echo " ✗ FAIL: --html-report flag not parsed correctly, output:" + echo "$output" + TESTS_FAILED=$((TESTS_FAILED + 1)) +fi +echo + +echo "========================================" +echo "Test Results:" +echo " Passed: $TESTS_PASSED" +echo " Failed: $TESTS_FAILED" +echo "========================================" + +if [ $TESTS_FAILED -gt 0 ]; then + echo "❌ Some tests failed!" + exit 1 +else + echo "✅ All tests passed!" + exit 0 +fi diff --git a/scripts/api-diff/test-api-diff.sh b/scripts/api-diff/test-api-diff.sh new file mode 100644 index 000000000..ac3086aea --- /dev/null +++ b/scripts/api-diff/test-api-diff.sh @@ -0,0 +1,142 @@ +#!/bin/bash + +# Script to check API diffs using openapi-changes +# Usage: ./scripts/api-diff/test-api-diff.sh [--fail-on-breaking] [filename.yaml] +# Assumes you have Docker installed and the repo is checked out with master branch available + +set -e # Exit on error +set -o pipefail # Catch errors in pipes + +# Change to repo root +cd "$(dirname "$0")/../.." + +# Configuration +DOCKER_IMAGE="${OPENAPI_CHANGES_DOCKER_IMAGE:-pb33f/openapi-changes:latest}" +BASE_BRANCH="${BASE_BRANCH:-origin/master}" + +FAIL_ON_BREAKING=false +TARGET_FILE="" + +# Parse arguments +for arg in "$@"; do + if [ "$arg" = "--fail-on-breaking" ]; then + FAIL_ON_BREAKING=true + elif [[ "$arg" == *.yaml ]]; then + TARGET_FILE="$arg" + fi +done + +echo "Starting API diff check..." + +# Ensure we're in the repo root +if [ ! -f "xero_accounting.yaml" ]; then + echo "Error: Not in repo root or xero_accounting.yaml not found" + exit 1 +fi + +# Fetch master if not already done +git fetch "${BASE_BRANCH%%/*}" "${BASE_BRANCH##*/}" 2>/dev/null || echo "Warning: Could not fetch ${BASE_BRANCH}" + +# Create temp directory for master branch files (outside repo to avoid overlap with /current mount) +TEMP_DIR=$(mktemp -d) +trap "rm -rf $TEMP_DIR" EXIT + +# Get list of xero*.yaml files (excluding any master_*.yaml files) +if [ -n "$TARGET_FILE" ]; then + # Single file specified + if [ ! -f "$TARGET_FILE" ]; then + echo "Error: File '$TARGET_FILE' not found" + exit 1 + fi + files="$TARGET_FILE" + echo "Running diff for single file: $TARGET_FILE" +else + # All xero*.yaml files + files=$(ls xero*.yaml 2>/dev/null | grep -v "^master_") + if [ -z "$files" ]; then + echo "No xero*.yaml files found" + exit 1 + fi +fi + +BREAKING_CHANGES_FOUND=false +FILES_WITH_BREAKING_CHANGES=() +TOTAL_FILES=0 +PROCESSED_FILES=0 + +echo "========================================" +echo "API Diff Summary" +echo "Using Docker image: $DOCKER_IMAGE" +echo "Base branch: $BASE_BRANCH" +echo "========================================" + +for file in $files; do + TOTAL_FILES=$((TOTAL_FILES + 1)) + echo "" + echo "========== $file ==========" + + # Get the file from master branch + if ! git show "$BASE_BRANCH:$file" > "$TEMP_DIR/$file" 2>/dev/null; then + echo "ℹ️ New file (does not exist in master branch)" + continue + fi + + # Verify the temp file was created + if [ ! -f "$TEMP_DIR/$file" ]; then + echo "❌ Failed to create temp file" + continue + fi + + # Note: openapi-changes provides deterministic results for change detection. + # Both error and warning counts are consistent between runs. + + # Run openapi-changes summary + echo "--- API Diff Summary ---" + set +e + SUMMARY_OUTPUT=$(docker run --rm -v "$(pwd)":/current -v "$TEMP_DIR":/base "$DOCKER_IMAGE" summary /base/"$file" /current/"$file" 2>&1) + SUMMARY_EXIT=$? + set -e + + echo "$SUMMARY_OUTPUT" + + if [ $SUMMARY_EXIT -eq 0 ]; then + echo "✓ No breaking changes detected" + else + echo "⚠ Breaking changes detected (exit code: $SUMMARY_EXIT)" + BREAKING_CHANGES_FOUND=true + FILES_WITH_BREAKING_CHANGES+=("$file") + fi + + PROCESSED_FILES=$((PROCESSED_FILES + 1)) +done + +echo "" +echo "========================================" +echo "API Diff check completed" +echo "Processed: $PROCESSED_FILES/$TOTAL_FILES files" +echo "========================================" + +# Summary +if [ "$BREAKING_CHANGES_FOUND" = true ]; then + echo "" + echo "❌ Breaking changes detected in the following files:" + for file in "${FILES_WITH_BREAKING_CHANGES[@]}"; do + echo " - $file" + # Output GitHub Actions annotation + if [ -n "$GITHUB_ACTIONS" ]; then + echo "::warning file=${file}::Breaking changes detected in this API spec file" + fi + done + + if [ "$FAIL_ON_BREAKING" = true ]; then + echo "" + echo "Exiting with error due to breaking changes" + exit 1 + else + echo "" + echo "Note: Not failing build (use --fail-on-breaking to fail on breaking changes)" + fi +else + echo "" + echo "✓ No breaking changes detected across all files" +fi \ No newline at end of file diff --git a/xero-webhooks.yaml b/xero-webhooks.yaml index b2a95a541..0f2b85def 100644 --- a/xero-webhooks.yaml +++ b/xero-webhooks.yaml @@ -163,4 +163,4 @@ components: "lastEventSequence": 1, "firstEventSequence": 1, "entropy": "S0m3r4Nd0mt3xt" - } + } \ No newline at end of file