Skip to content
Draft
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
85 changes: 85 additions & 0 deletions .github/workflows/openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
name: OpenAPI

on:
pull_request:
paths:
- "lib/realtime_web/**"
- "lib/mix/tasks/openapi.export.ex"
- "priv/openapi.json"
- "mix.exs"
- "mix.lock"
- ".github/workflows/openapi.yml"
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

concurrency:
group: openapi-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
drift_check:
name: Spec drift check
runs-on: ubuntu-24.04
timeout-minutes: 10

env:
MIX_ENV: dev

steps:
- uses: actions/checkout@v4

- name: Verify committed OpenAPI snapshot exists
run: |
if ! git ls-files --error-unmatch priv/openapi.json > /dev/null 2>&1; then
echo "::error file=priv/openapi.json::OpenAPI snapshot is not yet committed to the repository."
echo ""
echo "Bootstrap one-time: run 'mix openapi.export' locally and commit"
echo "priv/openapi.json. Or download the 'openapi-spec' artifact from a"
echo "previous run on this PR and commit it."
exit 1
fi

- uses: erlef/setup-beam@v1
with:
version-file: .tool-versions
version-type: strict

- uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ hashFiles('mix.lock') }}
restore-keys: |
${{ runner.os }}-mix-

- name: Install dependencies
run: |
mix local.hex --force
mix local.rebar --force
mix deps.get

- name: Regenerate OpenAPI spec
run: mix openapi.export

- name: Fail if committed spec is out of date
run: |
if ! git diff --exit-code -- priv/openapi.json; then
echo ""
echo "::error::The OpenAPI spec is out of date."
echo "Run 'mix openapi.export' locally and commit priv/openapi.json."
exit 1
fi

- name: Upload spec artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: openapi-spec
path: priv/openapi.json
if-no-files-found: warn
34 changes: 34 additions & 0 deletions lib/mix/tasks/openapi.export.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule Mix.Tasks.Openapi.Export do
@moduledoc """
Exports the RealtimeWeb OpenAPI spec to a JSON file on disk.

mix openapi.export # writes priv/openapi.json
mix openapi.export --output path.json # writes the given path

Used by CI to enforce that the committed spec stays in sync with the routes.
"""
use Mix.Task

@shortdoc "Exports the RealtimeWeb OpenAPI spec to a JSON file"

@default_output "priv/openapi.json"

@impl Mix.Task
def run(args) do
{opts, _, _} =
OptionParser.parse(args, strict: [output: :string], aliases: [o: :output])

output = Keyword.get(opts, :output, @default_output)

Mix.Task.run("app.start")

spec =
RealtimeWeb.ApiSpec.spec()
|> Jason.encode!(pretty: true)

output |> Path.dirname() |> File.mkdir_p!()
File.write!(output, spec <> "\n")

Mix.shell().info("Wrote OpenAPI spec to #{output}")
end
end
Loading