Một pull request chỉnh sửa openapi.yaml. Các kiểm tra CI đều thành công. Đặc tả hợp lệ, không có lỗi linter và được hai người đánh giá phê duyệt. Ba ngày sau, một ứng dụng di động bắt đầu gặp sự cố null-pointer vì một trường phản hồi từng tồn tại nay đã biến mất. Không ai cố ý xóa nó. Ai đó đã đổi tên một thuộc tính trong quá trình tái cấu trúc, và không có gì trong quá trình đánh giá đã phát hiện ra điều đó.
Đây là lỗ hổng mà một trình xác thực thông thường không bao giờ thấy. Một đặc tả có thể được định dạng hoàn hảo nhưng vẫn làm hỏng mọi ứng dụng tiêu thụ phụ thuộc vào nó. Cách duy nhất để biết là so sánh đặc tả mới với phiên bản mà nó thay thế, từng thay đổi một, và tự hỏi một câu: liệu điều này có làm hỏng một client đã hoạt động bình thường ngày hôm qua không? Sự so sánh đó chính là OpenAPI diff, và việc chạy nó như một cổng hợp nhất là một trong những kiểm tra mang lại hiệu quả cao nhất mà bạn có thể thêm vào kho lưu trữ API.
OpenAPI diff thực sự so sánh những gì
Một OpenAPI diff lấy hai đặc tả, một bản gốc (base) và một bản mới (head), và báo cáo những thay đổi giữa chúng. Bản gốc thường là đặc tả trên nhánh mục tiêu của bạn (đang hoạt động). Bản mới là đặc tả mà pull request của bạn đề xuất. Một công cụ diff tốt không chỉ xuất ra một bản delta văn bản như cách git diff làm. Nó hiểu cấu trúc OpenAPI, vì vậy nó có thể phân biệt giữa các chỉnh sửa thẩm mỹ và những thay đổi phá vỡ hợp đồng.
Đây là sự khác biệt quan trọng. Một số thay đổi là bổ sung và an toàn:
- Thêm một tham số yêu cầu tùy chọn mới
- Thêm một trường phản hồi mới
- Thêm một endpoint hoàn toàn mới
- Thêm một giá trị enum mới vào nội dung yêu cầu
Các client hiện có vẫn tiếp tục hoạt động qua tất cả những thay đổi đó. Chúng gửi những gì chúng luôn gửi và đọc những gì chúng luôn đọc. Các thay đổi khác không tương thích ngược, và đây là những thay đổi gây rắc rối:
- Xóa một trường phản hồi mà client đang đọc
- Đổi tên một thuộc tính (xóa cộng thêm một lần thêm, đối với một client)
- Biến một tham số trước đây là tùy chọn thành bắt buộc
- Thu hẹp một kiểu dữ liệu, như từ
stringsanginteger - Xóa một giá trị enum mà client có thể gửi
- Xóa một endpoint hoặc một phương thức HTTP
Nhiệm vụ của một công cụ OpenAPI diff là quét mọi đường dẫn, tham số, lược đồ và phản hồi trên cả hai tài liệu và phân loại mỗi thay đổi vào một trong các nhóm đó. Việc phân loại đó là toàn bộ mục đích. Một bản diff dòng thô sẽ chôn vùi một trường required bị xóa dưới năm mươi dòng định dạng lại. Một bản diff cấu trúc sẽ làm nổi bật nó như một thay đổi phá vỡ và cho bạn biết nó nằm dưới đường dẫn nào.
Nếu bạn muốn mô hình tư duy cơ bản về lý do tại sao một số thay đổi gây lỗi và một số khác thì không, hướng dẫn về cách quản lý phiên bản và loại bỏ API ở quy mô lớn sẽ đi sâu vào các quy tắc tương thích. Công cụ diff là cách bạn thực thi các quy tắc đó một cách máy móc thay vì hy vọng một người đánh giá sẽ nhớ chúng.
oasdiff: Công cụ mã nguồn mở hữu ích
oasdiff là công cụ mã nguồn mở mà hầu hết các nhóm sử dụng. Nó là một binary Go duy nhất, nhanh và được xây dựng đặc biệt để giải quyết vấn đề về thay đổi gây lỗi. Nó đọc các tài liệu OpenAPI 3.0 và 3.1 và cung cấp cho bạn một vài lệnh con tùy thuộc vào những gì bạn muốn từ việc so sánh.
Ba lệnh bạn sẽ sử dụng nhiều nhất:
diffbáo cáo toàn bộ các khác biệt giữa hai đặc tả.breakingchỉ báo cáo các thay đổi không tương thích ngược.changelogtạo ra một danh sách dễ đọc của mọi thay đổi đáng kể, dù có gây lỗi hay không.
Đối với một cổng hợp nhất, breaking là cái quan trọng. Hãy trỏ nó vào đặc tả gốc và đặc tả mới của bạn:
oasdiff breaking base-openapi.yaml head-openapi.yaml --fail-on ERR
base-openapi.yaml là đặc tả từ nhánh mục tiêu và head-openapi.yaml là đặc tả trong pull request. Lệnh con breaking chỉ in các thay đổi không tương thích. Cờ --fail-on ERR là thứ biến điều này thành một cổng: nó khiến lệnh thoát với trạng thái khác 0 khi nó tìm thấy một thay đổi được phân loại ở mức ERR. Thoát khác 0 là tín hiệu phổ quát mà CI đọc là lỗi.
Mô hình mức độ nghiêm trọng đó rất đáng để hiểu. oasdiff phân loại các thay đổi gây lỗi thành các cấp độ, và ERR là cấp độ nghiêm trọng, một thay đổi sẽ làm hỏng client. WARN bao gồm các thay đổi có thể làm hỏng một số client tùy thuộc vào cách chúng được viết, và INFO là thông tin. Bạn quyết định nên đặt ranh giới ở đâu. --fail-on ERR chỉ chặn những lỗi chắc chắn. --fail-on WARN nghiêm ngặt hơn và cũng bắt cả những lỗi có thể xảy ra.
Khi bạn muốn một bản tóm tắt dễ đọc cho changelog hoặc bình luận PR thay vì chỉ là pass/fail, lệnh con changelog sẽ cho ra kết quả thân thiện hơn:
oasdiff changelog base-openapi.yaml head-openapi.yaml
oasdiff có một vài điểm thực sự hữu ích. Nó thực hiện khớp endpoint mà vẫn tồn tại các tham số đường dẫn được đổi tên, vì vậy nó không đánh dấu {userId} trở thành {id} là một hành động xóa-cộng-thêm khi đường dẫn khác giống hệt. Nó có thể hợp nhất các lược đồ allOf trước khi so sánh để thừa kế không tạo ra nhiễu. Và nó xuất ra nhiều định dạng hơn là văn bản thuần túy: HTML, JSON, YAML và Markdown đều có sẵn thông qua các cờ output, giúp dễ dàng đưa kết quả vào chú thích CI hoặc changelog được tạo tự động. Đối với một công cụ bạn có thể đưa vào một pipeline trong năm phút và tin tưởng vào việc nó sẽ thận trọng về những gì nó gọi là lỗi phá vỡ, thật khó để tìm được công cụ nào tốt hơn.
openapi-diff: Thay thế JVM
Nếu stack của bạn đã chạy trên JVM, OpenAPITools/openapi-diff là một lựa chọn thứ hai vững chắc và đáng để tìm hiểu. Đây là một công cụ dựa trên Java (Java 8 trở lên) so sánh hai đặc tả OpenAPI 3.x và hiển thị sự khác biệt dưới dạng HTML, Markdown, AsciiDoc, JSON hoặc văn bản console. Bạn có thể chạy nó từ một file jar đã build, thông qua Maven, qua Homebrew hoặc dưới dạng một image Docker, vì vậy nó phù hợp với nhiều thiết lập build mà không gặp nhiều rắc rối.
Sự so sánh của nó đi sâu vào các tham số, phản hồi, endpoint và phương thức HTTP, và nó vạch ra cùng một ranh giới mà mọi người đều quan tâm: các thay đổi giữ được khả năng tương thích ngược so với các thay đổi làm hỏng nó. CLI rất đơn giản:
openapi-diff old-openapi.yaml new-openapi.yaml --fail-on-incompatible
Cờ --fail-on-incompatible chỉ thoát khác không khi một thay đổi làm hỏng tính tương thích ngược, đây chính xác là hành vi cổng bạn muốn. Có một cờ --fail-on-changed nghiêm ngặt hơn nếu bạn muốn thất bại với bất kỳ thay đổi nào, và một chế độ --state chỉ in no_changes, compatible hoặc incompatible khi bạn muốn một câu trả lời một từ để tự động hóa.
Điểm mạnh của nó nằm ở đầu ra được render. Các báo cáo HTML và Markdown rõ ràng và chi tiết, điều này khiến openapi-diff trở thành lựa chọn mạnh mẽ khi bạn muốn một artifact diff mà con người thực sự sẽ đọc, chứ không chỉ là mã thoát của CI. Đánh đổi là sự phụ thuộc vào JVM và thời gian khởi động nặng hơn so với một binary Go. Nếu nhóm của bạn đã là một team Java, thì chi phí đó là bằng không và công cụ này hoạt động tốt. Nếu không, oasdiff nhẹ nhàng hơn. Cả hai đều trả lời tốt câu hỏi về thay đổi gây lỗi; hãy chọn công cụ phù hợp với môi trường runtime bạn đang duy trì.
Kết nối diff vào CI như một cổng hợp nhất
Một bản diff mà bạn chạy bằng tay sẽ không phát hiện được gì, bởi vì thời điểm bạn quên chạy nó là thời điểm lỗi được triển khai. Cổng này phải nằm trong pipeline và kích hoạt trên mọi pull request chạm đến đặc tả.
Một điểm phức tạp trong CI là bạn cần cả hai phiên bản của đặc tả cùng lúc: bản gốc từ nhánh mục tiêu và bản mới từ PR. Việc kiểm tra PR sẽ cung cấp cho bạn bản mới. Bạn lấy bản gốc trực tiếp từ lịch sử git mà không cần kiểm tra lại:
name: openapi-diff
on:
pull_request:
paths:
- "openapi.yaml"
jobs:
breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get base spec
run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml
- name: Install oasdiff
run: |
curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- name: Diff for breaking changes
run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR
Một vài chi tiết quan trọng ở đây. fetch-depth: 0 kéo toàn bộ lịch sử để git show có thể tiếp cận nhánh gốc. Dòng git show origin/<base>:openapi.yaml đọc đặc tả như nó tồn tại trên nhánh mục tiêu và ghi nó vào một tệp, không cần thêm bản clone nào. Bộ lọc paths có nghĩa là công việc chỉ chạy khi đặc tả thực sự thay đổi, vì vậy các PR không liên quan sẽ không phải chịu chi phí. Và bước cuối cùng là cổng: nếu oasdiff breaking tìm thấy một thay đổi ở mức ERR, nó sẽ thoát với mã không phải 0, công việc chuyển sang màu đỏ và PR hiển thị một kiểm tra thất bại trước khi bất kỳ ai nhấp vào hợp nhất.
Tác giả thấy chính xác thay đổi nào đã phá vỡ khả năng tương thích, trên đường dẫn nào, trong khi mã vẫn đang được xem xét. Đó là toàn bộ giá trị. Lỗi được phát hiện vào thời điểm rẻ nhất có thể thay vì trong báo cáo sự cố của khách hàng.
Tất nhiên, không phải mọi thay đổi gây lỗi đều là một sai lầm. Đôi khi bạn đang phát hành một phiên bản chính có chủ đích và lỗi đó là cố ý. Mô hình rõ ràng là đóng cổng theo mặc định và yêu cầu ghi đè rõ ràng cho các trường hợp ngoại lệ: một nhãn trên PR, một bản cập nhật phiên bản trong info.version, hoặc một quy trình làm việc được phê duyệt riêng. Bằng cách đó, một lỗi luôn là một quyết định ai đó đã đưa ra một cách cố ý, chứ không bao giờ là một tai nạn đã lọt qua. Hướng dẫn chiến lược quản lý phiên bản API sẽ trình bày khi nào một lỗi xứng đáng có một phiên bản chính mới và khi nào nên tránh nó.
Khoảng cách mà Diff không thể thu hẹp
Đây là giới hạn của mọi công cụ nêu trên, và nó rất quan trọng. Một diff so sánh hai tệp. Nó cho bạn biết rằng tài liệu mới tương thích ngược với tài liệu cũ. Nó không nói gì về việc dịch vụ đang chạy của bạn có thực sự khớp với một trong hai tài liệu đó hay không.
Đó là một lỗi khác, và là lỗi gây ra vấn đề nghiêm trọng nhất trong môi trường sản phẩm. Đặc tả hứa hẹn một trường created_at; nhưng việc triển khai đã lặng lẽ ngừng trả về nó ba sprint trước. Đặc tả nói rằng một endpoint trả về 200; dịch vụ trực tiếp trả về 500 trong một điều kiện mà không ai kiểm tra. Diff sạch vì cả hai phiên bản đặc tả đều đồng ý. Hợp đồng và mã không khớp. Một diff tĩnh không thể biết, vì nó không bao giờ giao tiếp với API.
Để thu hẹp khoảng cách đó có nghĩa là kiểm tra API đang hoạt động so với hợp đồng, chứ không chỉ so sánh hợp đồng với chính nó. Bạn tạo các bài kiểm tra từ đặc tả, chạy chúng trên dịch vụ đang hoạt động và xác nhận rằng các phản hồi thực tế khớp với các hình dạng đã được ghi lại. Đó là kiểm thử hợp đồng, và đó là lớp bắt lỗi sai lệch giữa những gì bạn đã viết và những gì bạn thực sự đã triển khai.
Thu hẹp khoảng cách bằng Apidog và Apidog CLI
Apidog được xây dựng cho vòng lặp này, điều này khiến nó trở thành một người bạn đồng hành tự nhiên cho bước diff thay vì thay thế nó. Bạn nhập hoặc đồng bộ hóa đặc tả OpenAPI của mình vào một dự án Apidog, và Apidog có thể tạo kịch bản kiểm thử trực tiếp từ đặc tả, với các xác nhận được lấy từ lược đồ. Các bài kiểm thử kiểm tra xem các phản hồi thực tế có khớp với các loại được ghi lại, các trường bắt buộc và mã trạng thái hay không. Bạn xây dựng và duy trì các kịch bản đó một cách trực quan thay vì viết tay một bộ script kiểm thử song song bị lệch mỗi khi hợp đồng thay đổi.
Vì Apidog giữ thiết kế, mocking và kiểm thử trong một không gian làm việc, đặc tả vẫn là nguồn chân lý trên tất cả chúng. Bạn có thể tải xuống Apidog và nhập một đặc tả hiện có để thử vòng lặp trên API của riêng bạn. Nếu bạn vẫn đang quyết định cách kiểm soát đặc tả đó trên các phiên bản ngay từ đầu, hướng dẫn về kiểm soát phiên bản đặc tả OpenAPI bằng Git rất phù hợp với quy trình làm việc này.
Apidog CLI là công cụ chạy các kịch bản đó một cách không giao diện trong pipeline của bạn. Đó là một gói npm:
npm install -g apidog-cli
Bạn chạy một kịch bản theo ID, trỏ nó đến môi trường bạn muốn xác thực và yêu cầu một báo cáo thân thiện với CI:
apidog run \
--access-token $APIDOG_ACCESS_TOKEN \
-t <scenarioId> \
-e <environmentId> \
-r junit,cli \
--out-dir ./apidog-reports
Mã thông báo truy cập xác thực quá trình chạy và nằm trong một bí mật CI, không bao giờ trong một tệp đã commit. Cờ -t chọn kịch bản, -e chọn môi trường và -r junit,cli xuất XML JUnit có thể đọc được bằng máy cho bảng điều khiển CI của bạn cùng với đầu ra terminal dễ đọc cho nhật ký build. Bạn không đoán các ID: bạn sao chép lệnh chính xác, với các ID kịch bản và môi trường thực tế đã được điền, từ tab CI/CD của kịch bản trong Apidog. Nếu bạn muốn toàn bộ các tùy chọn, hướng dẫn CLI đầy đủ ghi lại mọi cờ và apidog run --help in chúng theo yêu cầu.
Hành vi cổng có nguyên tắc giống như diff. Khi một xác nhận thất bại, vì phản hồi trực tiếp không còn khớp với hợp đồng, apidog run sẽ thoát với mã khác không. CI đọc mã thoát, đánh dấu bước đó là thất bại và chặn việc hợp nhất. Không cần cấu hình bổ sung. Miễn là bước chạy nằm trong pipeline, lỗi hồi quy hợp đồng sẽ dừng dòng giống như cách diff thay đổi gây lỗi.
Trình tự đầy đủ trước khi hợp nhất
Kết hợp hai phần lại với nhau và bạn sẽ có một pipeline bắt được cả hai loại lỗi. Diff phát hiện các thay đổi có thể làm hỏng client bằng cách đọc đặc tả. Kiểm thử hợp đồng phát hiện một dịch vụ không còn tuân thủ đặc tả bằng cách thực hiện API đang chạy. Hãy chạy chúng như các công việc riêng biệt:
jobs:
breaking-changes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: git show origin/${{ github.base_ref }}:openapi.yaml > base-openapi.yaml
- run: curl -fsSL https://raw.githubusercontent.com/oasdiff/oasdiff/main/install.sh | sh
- run: oasdiff breaking base-openapi.yaml openapi.yaml --fail-on ERR
contract-conformance:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm install -g apidog-cli
- name: Run contract tests
run: |
apidog run \
--access-token "$APIDOG_ACCESS_TOKEN" \
-t 605067 \
-e 1629989 \
-r junit,cli \
--out-dir ./apidog-reports
env:
APIDOG_ACCESS_TOKEN: ${{ secrets.APIDOG_ACCESS_TOKEN }}
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: apidog-report
path: ./apidog-reports
Hai công việc chạy song song. Công việc diff đọc tệp và chỉ cần git, vì vậy nó hoàn thành trong vài giây. Công việc kiểm tra sự tuân thủ cần một môi trường có thể truy cập, vì vậy nó thường chạy trên một bản build staging đã triển khai. Lệnh if: always() trên phần upload giữ cho báo cáo được tạo ngay cả khi các bài kiểm thử thất bại, đây chính là lúc bạn muốn đọc nó. Nếu một trong hai công việc chuyển sang màu đỏ, PR sẽ bị chặn. Để biết thêm về việc chạy CLI trong các pipeline thực tế, hướng dẫn GitHub Actions của Apidog CLI và hướng dẫn pipeline CI/CD rộng hơn sẽ đi sâu hơn vào cách thiết lập.
