GitLab Grape OpenAPI

[!IMPORTANT] Internal use only. This gem exists to generate the OpenAPI 3.0 spec for the GitLab Rails monorepo and is not intended for use outside of GitLab.

  • It is published to rubygems.org only so the monorepo's Gemfile can depend on it — not as a general-purpose Grape → OpenAPI tool.
  • The public API, configuration DSL, and generated output may change in any release, including patch releases. There is no semantic-versioning contract.
  • No external support is provided. Issues and merge requests from outside GitLab may be closed without review.
  • Feature work is driven by the needs of gitlab-org/gitlab; capabilities that aren't needed there will not be added.

Internal gem for generating OpenAPI 3.0 specifications from Grape API definitions, used by gitlab-org/gitlab to publish its REST API reference.

Installation

Add to your Gemfile:

gem 'gitlab-grape-openapi'

Then run:

bundle install

Configuration

Configure the gem using the Gitlab::GrapeOpenapi.configure block, typically in an initializer:

Gitlab::GrapeOpenapi.configure do |config|
  # Required: API metadata
  config.info = Gitlab::GrapeOpenapi::Models::Info.new(
    title: 'My API',
    description: 'API description',
    version: 'v1',
    terms_of_service: 'https://example.com/terms'
  )

  # API path configuration
  config.api_prefix = "api"    # Default: "api"
  config.api_version = "v1"    # Default: "v1"

  # Server definitions
  config.servers = [
    Gitlab::GrapeOpenapi::Models::Server.new(
      url: 'https://{hostname}',
      description: "Production API",
      variables: {
        hostname: Gitlab::GrapeOpenapi::Models::ServerVariable.new(
          default: 'api.example.com',
          description: 'API hostname'
        )
      }
    )
  ]

  # Security schemes
  config.security_schemes = [
    Gitlab::GrapeOpenapi::Models::SecurityScheme.new(
      name: "bearerAuth",
      type: "http",
      scheme: "bearer"
    )
  ]

  # Exclude specific API classes from generation
  config.excluded_api_classes = [
    'API::Internal::Base',
    'API::Internal::Admin'
  ]

  # Override tag names for better display
  config.tag_overrides = {
    'Ci' => 'CI',
    'Oauth' => 'OAuth'
  }

  # Map Grape route settings to OpenAPI extensions
  config.annotations = {
    lifecycle: 'x-gitlab-lifecycle'
  }
end

Configuration Options

Option Type Default Description
info Models::Info nil API metadata (title, description, version, terms of service)
api_prefix String "api" URL prefix for API routes
api_version String "v1" API version string
servers Array<Models::Server> [] Server definitions for the API
security_schemes Array<Models::SecurityScheme> [] Authentication/authorization schemes
excluded_api_classes Array<String> [] API class names to exclude from generation
tag_overrides Hash {} Map of tag names to their display overrides
annotations Hash {} Map of Grape route settings to OpenAPI extension names
warnings Boolean false Emit stderr warnings for synthesized (undeclared) path params

Annotations

The annotations configuration maps Grape route settings to OpenAPI vendor extensions. For example:

config.annotations = {
  lifecycle: 'x-gitlab-lifecycle'
}

When a Grape endpoint has:

```ruby
route_setting :lifecycle, 'mature'

The generated OpenAPI spec will include:

x-gitlab-lifecycle: mature

Usage

Generating an OpenAPI Specification

# Load all API and entity classes
Rails.application.eager_load!

api_classes = API::Base.descendants
entity_classes = Grape::Entity.descendants

# Generate the specification
spec = Gitlab::GrapeOpenapi.generate(
  api_classes: api_classes,
  entity_classes: entity_classes
)

# Output as JSON
File.write('openapi.json', JSON.pretty_generate(spec))

# Or as YAML
require 'yaml'
File.write('openapi.yaml', spec.to_yaml)

Usage with gitlab-org/gitlab

  1. Start a Rails console in your GDK:
   cd ~/gdk/gitlab
   rails console
  1. Generate the OpenAPI specification:
   Rails.application.eager_load!
   api_classes = API::Base.descendants
   entity_classes = Grape::Entity.descendants
   spec = Gitlab::GrapeOpenapi.generate(api_classes: api_classes, entity_classes: entity_classes)
   File.write(Rails.root.join('tmp', 'openapi.json'), JSON.pretty_generate(spec))
  1. The spec will be saved to tmp/openapi.json in your GitLab directory.

Architecture

The gem follows a converter-based architecture:

Generator
├── TagConverter        - Extracts tags from API classes
├── EntityConverter     - Converts Grape::Entity to OpenAPI schemas
├── PathConverter       - Converts routes to OpenAPI paths
│   ├── OperationConverter  - Converts individual endpoints
│   ├── ParameterConverter  - Converts endpoint parameters
│   ├── ResponseConverter   - Converts endpoint responses
│   └── RequestBodyConverter - Converts request bodies
└── TypeResolver        - Maps Ruby/Grape types to OpenAPI types

Registries

  • SchemaRegistry - Tracks converted entity schemas
  • RequestBodyRegistry - Tracks request body schemas
  • TagRegistry - Tracks API tags

Development

bundle install
bundle exec rspec

Running Tests

bundle exec rspec

Linting

bundle exec rubocop

Releasing

Releases are driven by the gem-release CI component and a merge request whose title contains RELEASE.

  1. Open a merge request from the Release template. The /title RELEASE: v<NEW_VERSION> quick action ensures the title contains RELEASE, which is what exposes the release-creation and gem-publication jobs.
  2. Bump the VERSION constant in lib/gitlab/grape_openapi/version.rb and fill in the changelog per the template.
  3. Get the MR reviewed and merged. Do not run the gem-publication job from the MR pipeline — it is meant to stay un-run during review (see below).
  4. After merge, follow the post-release steps in CLAUDE.md to bump the gitlab-grape-openapi version in gitlab-org/gitlab and regenerate the committed openapi_v3.yaml.

How publishing works

GEM_HOST_API_KEY is configured as an inherited, protected variable on a parent group, so it is only exposed to pipelines running on protected branches or protected tags. This is why running the gem-publication job manually from the MR pipeline fails — the API key is not available there.

The expected flow relies on the default branch being protected:

  • During review, the MR-pipeline gem-publication job stays un-run (it is manual, and would fail anyway without the key).
  • When the version bump lands on the default branch (main), the gem-publication job runs again — this time with the protected GEM_HOST_API_KEY available — and publishes the gem to RubyGems.

The gem-release component's rules that re-trigger publication after merge:

.gem-release-default-rules:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      changes: ["lib/**/version.rb"]
    - if: '$CI_MERGE_REQUEST_TITLE =~ /RELEASE/ && "...dry_run..." == "true"'
    - if: '$CI_MERGE_REQUEST_TITLE =~ /RELEASE/'
      when: manual

The first rule is the one that publishes: a push to the default branch that changes lib/**/version.rb.

Contributing

This gem is maintained by GitLab's API Platform team for internal use. External contributions are not actively solicited; issues and merge requests opened by non-GitLab contributors may be closed without review. GitLab team members should follow the standard contribution guidelines — see the project page.

License

Released under the MIT License.