Skip to content
Open
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
102 changes: 102 additions & 0 deletions .github/workflows/openapi_conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Check the traffic our request specs already generate against the OpenAPI
# description in docs/openapi, and report what it finds.
#
# Informational: the description already disagrees with the API in a handful
# of places, so the run publishes a digest rather than failing. Dispatch it
# with `strict` to see what gating would do.
name: OpenAPI Conformance

concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.run_id }}'
cancel-in-progress: true

on:
workflow_dispatch:
inputs:
strict:
description: 'Fail the job on a non-conforming response'
type: boolean
default: false
pull_request:
branches: [ main ]
paths:
- 'app/**'
- 'lib/**'
- 'docs/openapi/**'
- 'spec/request/**'
- 'spec/support/openapi_conformance.rb'
- 'spec/support/request_spec_helper.rb'
- '.github/workflows/openapi_conformance.yml'

permissions:
contents: read

jobs:
Conformance:
name: Request specs vs. OpenAPI description
runs-on: ubuntu-latest
timeout-minutes: 45
services:
postgres:
image: postgres:17
env:
POSTGRES_PASSWORD: rootpassword
# Test-only durability tuning: skip the work that backs durability.
command: >-
postgres
-c fsync=off
-c synchronous_commit=off
-c full_page_writes=off
options: >-
--tmpfs /var/lib/postgresql/data:rw,size=2g
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
env:
DB: postgres
POSTGRES_CONNECTION_PREFIX: postgres://postgres:rootpassword@localhost:5432
OPENAPI_CONFORMANCE: ${{ (github.event_name == 'workflow_dispatch' && inputs.strict) && 'strict' || '1' }}
steps:
- uses: actions/checkout@v7
- uses: ./.github/workflows/composite/setup
- uses: actions/setup-node@v7
with:
node-version: 24

# openapi_first takes a single file, so the multi-file redocly source
# has to be bundled before the specs can read it.
- name: Build the API description
working-directory: docs/openapi
run: |
yarn install --frozen-lockfile
yarn build

- name: Prepare the database
run: bundle exec rake db:recreate

- name: Run request specs against the description
run: bundle exec rspec spec/request --format progress

- name: Publish the report
if: always()
run: |
if [ -f out/openapi_conformance.md ]; then
cat out/openapi_conformance.md >> "$GITHUB_STEP_SUMMARY"
else
echo '## OpenAPI conformance' >> "$GITHUB_STEP_SUMMARY"
echo 'No report produced -- the run failed before the specs finished.' >> "$GITHUB_STEP_SUMMARY"
fi

- name: Upload the report
if: always()
uses: actions/upload-artifact@v7
with:
name: openapi-conformance
path: |
out/openapi_conformance.md
out/openapi_coverage.html
if-no-files-found: warn
retention-days: 7
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ end
group :test do
gem 'factory_bot', '~> 6.5'
gem 'mock_redis'
gem 'openapi_first'
gem 'parallel_tests'
gem 'rack-test'
gem 'rspec', '~> 3.13.2'
Expand Down
13 changes: 13 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ GEM
grpc (1.84.0-x86_64-linux-gnu)
google-protobuf (>= 3.25, < 5.0)
googleapis-common-protos-types (~> 1.0)
hana (1.3.7)
hashdiff (1.2.1)
highline (3.0.1)
httpclient (2.9.0)
Expand All @@ -127,6 +128,11 @@ GEM
json-schema (6.2.0)
addressable (~> 2.8)
bigdecimal (>= 3.1, < 5)
json_schemer (2.5.0)
bigdecimal
hana (~> 1.3)
regexp_parser (~> 2.0)
simpleidn (~> 0.2)
kramdown (2.5.2)
rexml (>= 3.4.4)
kramdown-parser-gfm (1.1.0)
Expand Down Expand Up @@ -174,6 +180,11 @@ GEM
bigdecimal (>= 3.0)
ostruct (>= 0.2)
open3 (0.2.1)
openapi_first (4.0.1)
drb (~> 2.0)
hana (~> 1.3)
json_schemer (>= 2.1, < 3.0)
rack (>= 2.2, < 4.0)
openssl (4.0.2)
ostruct (0.6.3)
parallel (2.3.0)
Expand Down Expand Up @@ -353,6 +364,7 @@ GEM
sequel (>= 4.38.0)
sexp_processor (4.17.6)
simplecov (1.3.2)
simpleidn (0.3.0)
sinatra (4.2.1)
logger (>= 1.6.0)
mustermann (~> 3.0)
Expand Down Expand Up @@ -462,6 +474,7 @@ DEPENDENCIES
newrelic_rpm
nokogiri (>= 1.10.5)
oj
openapi_first
openssl (>= 3.2)
parallel_tests
pg
Expand Down
39 changes: 39 additions & 0 deletions docs/openapi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,45 @@ This is useful for:
- Validating a mock server's implementation against the OpenAPI spec.
- Quickly checking a live API for compliance without running the full `capi-bara-tests` suite.

## Checking the description against the request specs

The Cloud Controller's rspec request specs drive the real rack app, so every
example is already a request/response pair that this description either covers
or doesn't. `spec/support/openapi_conformance.rb` wraps that app with
[openapi_first](https://rubygems.org/gems/openapi_first) and records the
verdict -- no deployed CF, no proxy, no traffic capture.

It is off unless `OPENAPI_CONFORMANCE` is set, and it reads the bundled
description, so build that first:

```bash
cd docs/openapi && yarn install && yarn build
cd ../.. && DB=postgres OPENAPI_CONFORMANCE=1 bundle exec rspec spec/request
```

That writes `out/openapi_conformance.md` (a digest of coverage and violations)
and `out/openapi_coverage.html` (every route, with the ones nothing exercised).
`OPENAPI_CONFORMANCE=strict` turns a non-conforming response into a spec
failure instead of a line in the report.

Violations reading `does not match format: uuid` are counted but not listed.
Around 250 places in `spec/request` hand a model a literal guid such as
`'app1_guid'` and then assert on it, so that check only has signal against
recorded traffic, not against fixtures.

5xx responses are left out of coverage. They are described on nearly every
operation, but request specs almost never produce them, so they only drag the
number down; the HTML report lists them as skipped. A skipped response isn't
tracked, so a malformed 5xx body won't show up as a violation either.

The `OpenAPI Conformance` GitHub Action runs this on pull requests and
publishes the digest to the job summary. The step log carries the same digest
plus, in a collapsed "OpenAPI coverage details" group, every route that isn't
fully covered -- the HTML report is also uploaded as an artifact, but there is
no need to download it. It is informational -- the
description already disagrees with the API in a handful of places, so there is
nothing honest to gate on yet.

## Contributing

Contributions are welcome! Please feel free to submit a pull request or open an issue to discuss any changes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ properties:
type: object
properties:
guid:
type: string
type: [string, "null"]
description: Unique identifier of the app that this event pertains to, if applicable
name:
type: [string, "null"]
Expand Down Expand Up @@ -57,7 +57,7 @@ properties:
type: object
properties:
guid:
type: string
type: [string, "null"]
description: Unique identifier of the task that this event pertains to, if applicable
name:
type: [string, "null"]
Expand Down
2 changes: 1 addition & 1 deletion docs/openapi/apis/cf/latest/components/schemas/Build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ allOf:
type: object
properties:
guid:
type: string
type: [string, "null"]
description: The guid of the user that created the build
name:
type: [string, "null"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ allOf:
type: object
properties:
guid:
type: string
type: [string, "null"]
format: uuid
description: "The app\u2019s [current droplet guid](#get-current-droplet-association-for-an-app) before the deployment was created"
new_processes:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ type: object
properties:
data:
type: [object, "null"]
$ref: './Relationship.yaml'
properties:
guid:
type: string
format: uuid
description: The GUID of the resource
links:
type: object
properties:
Expand Down
3 changes: 1 addition & 2 deletions docs/openapi/apis/cf/latest/paths/Processes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,7 @@
- STARTING
- DOWN
since:
type: number
format: double
type: integer
links:
type: object
properties:
Expand Down
2 changes: 1 addition & 1 deletion spec/request/processes_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@
end

let(:instances_reporters) { double(:instances_reporters) }
let(:usage_time) { Time.now.utc.to_s }
let(:usage_time) { Time.now.utc.to_datetime.rfc3339 }

let(:expected_response) do
{
Expand Down
Loading
Loading