Skip to content
Merged
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
210 changes: 210 additions & 0 deletions .github/workflows/oas.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# Run quality control on the (generated) Open API specification.
#
# This workflow is intended to replace:
# - generate-postman-collection.yml
# - generate-sdks.yml
# - lint-oas.yml
# - oas-check.yml
#
# The OAS must:
# * not be outdated w/r to the code from which it is generated
# * not have any linting errors
# * be valid input to generate a Postman collection
# * be valid input to generate SDKs in commonly used languages/frameworks
#
# When dealing with multiple versions, you can adapt this workflow to run a matrix and
# pass arguments down that way, and/or use a parent workflow to call this workflow for
# each matrix item. See https://docs.github.com/en/actions/sharing-automations/reusing-workflows

name: "Check OAS"

on:
workflow_call:
inputs:
python-version:
type: string
required: false
description: Python version, passed to actions/setup-python
default: '3.12'

django-settings-module:
required: true
type: string

oas-generate-command:
type: string
required: false
description: >
(Binary) command to run to generate the schema. If working-directory is specified,
it's relative to this directory.
default: 'bin/generate_schema.sh'

schema-path:
type: string
required: false
description: Location of the OAS file, relative to working-directory if specified.
default: 'src/openapi.yaml'

oas-artifact-name:
type: string
required: false
description: Name for the artifact of the generated schema.
default: generated-oas

apt-packages:
type: string
required: false
description: Any additional apt packages to install, space-separated
default: ''

working-directory:
type: string
required: false
description: Specifies the working directory where commands are run.
default: ''

node-version-file:
type: string
required: false
description: >2
Passed down to actions/setup-node, so the same rules apply. Alternatively, use
`node-version` instead.
default: ''

node-version:
type: string
required: false
description: >2
Passed down to actions/setup-node, so the same rules apply. Alternatively, use
`node-version-file` instead.
default: ''

spectral-version:
type: string
required: false
description: >2
The NPM package version number of spectral-cli to install. It's recommended
to pin this for stability reasons.
default: '^6.15.0'

spectral-ruleset:
type: string
required: false
description: >2
Path or URL to the ruleset to use. The default value (`.spectral.yaml`) is created
if it doesn't exist.
default: .spectral.yaml

openapi-to-postman-version:
type: string
required: false
description: >2
The NPM package version number of openapi-to-postmanv2 to install. It's recommended
to pin this for stability reasons.
default: '^5.0.0'

postman-artifact-name:
type: string
required: false
description: >2
Name of the artifact to upload with the generated collection. Artifact uploads are
skipped if no name is provided.
default: ''

openapi-generator-version:
type: string
required: false
description: >2
The NPM package version number of @openapitools/openapi-generator-cli to install.
It's recommended to pin this for stability reasons.
default: '^2.20.0'

openapi-generator-config:
type: string
required: false
description: >2
Path or URL to the config to use. The default value (`openapitools.json`) is created
if it doesn't exist.
default: openapitools.json

jobs:

generate-and-compare:
name: Generate OAS and check staleness
runs-on: ubuntu-latest

outputs:
schema-path: ${{ steps.generate.outputs.schema-path }}

steps:
- name: Generate
uses: maykinmedia/open-api-workflows/actions/oas-generate@refactor/reusable-actions
id: generate
with:
python-version: ${{ inputs.python-version }}
command: ${{ inputs.oas-generate-command }}
artifact-name: ${{ inputs.oas-artifact-name }}
schema-path: ${{ inputs.schema-path }}
apt-packages: ${{ inputs.apt-packages }}
working-directory: ${{ inputs.working-directory }}
env:
DJANGO_SETTINGS_MODULE: ${{ inputs.django-settings-module }}

- name: Compare
uses: maykinmedia/open-api-workflows/actions/oas-compare@refactor/reusable-actions
with:
artifact-name: ${{ inputs.oas-artifact-name }}
schema-path: ${{ inputs.schema-path }}
working-directory: ${{ inputs.working-directory }}

lint:
name: Lint OAS
runs-on: ubuntu-latest
needs:
- generate-and-compare # no point in linting something that's not up to date

steps:
- uses: actions/checkout@v4
- name: Lint
uses: maykinmedia/open-api-workflows/actions/oas-lint@refactor/reusable-actions
with:
schema-path: ${{ needs.generate-and-compare.outputs.schema-path }}
node-version-file: ${{ inputs.node-version-file }}
node-version: ${{ inputs.node-version }}
spectral-version: ${{ inputs.spectral-version }}
ruleset: ${{ inputs.spectral-ruleset }}

postman-collection:
name: Generate Postman collection
runs-on: ubuntu-latest
needs:
- generate-and-compare # no point in linting something that's not up to date

steps:
- uses: actions/checkout@v4
- name: Generate collection
uses: maykinmedia/open-api-workflows/actions/oas-to-postman@refactor/reusable-actions
with:
schema-path: ${{ needs.generate-and-compare.outputs.schema-path }}
node-version-file: ${{ inputs.node-version-file }}
node-version: ${{ inputs.node-version }}
openapi-to-postman-version: ${{ inputs.openapi-to-postman-version }}
artifact-name: ${{ inputs.postman-artifact-name }}

sdks:
name: Generate SDKS
runs-on: ubuntu-latest

needs:
- generate-and-compare # no point in linting something that's not up to date

steps:
- uses: actions/checkout@v4
- name: SDK generation
uses: maykinmedia/open-api-workflows/actions/oas-sdks@refactor/reusable-actions
with:
schema-path: ${{ needs.generate-and-compare.outputs.schema-path }}
node-version-file: ${{ inputs.node-version-file }}
node-version: ${{ inputs.node-version }}
openapi-generator-version: ${{ inputs.openapi-generator-version }}
config: ${{ inputs.openapi-generator-config }}
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# open-api-workflows
[Reusable workflows](/.github/workflows/) for several open-api related projects.
The workflows consists of several jobs which ran similiar across several different
repositories.

## Usage
[Reusable workflows](/.github/workflows/) and [actions](./actions) for projects that
implement an API.

The re-usable workflows compose a number of jobs that should be kept similar across
a multitude of repositories, while the actions provide lower-level building blocks
that are also useful in projects that aren't purely API-focused.

## Usage (workflows)

```yaml
# ci.yml
Expand Down
22 changes: 22 additions & 0 deletions actions/extract-version/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Extract version information

Small script/action to extract `version` and `git_sha` outputs from the Git and Github
state that triggered the job/workflow.

Typically you want to collect this information once and then re-use it to tag your
build artifacts like Docker images, or you may need to derive other information from it.

## Example usage

```yaml
jobs:
setup:
name: Set up the build variables
runs-on: ubuntu-latest
outputs:
version: ${{ steps.vars.outputs.version }}
git_hash: ${{ steps.vars.outputs.git_hash }}
steps:
- uses: maykinmedia/open-api-workflows/actions/extract-version@refactor/reusable-actions
id: vars
```
51 changes: 51 additions & 0 deletions actions/extract-version/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---

name: Extract version information from the git state

description: >2
Given the checked out repository and github event, determine which "version" is being
built in CI.

Pull requests and builds on the default branch (main/master) resolve to the version
`latest`, while git tags are used as value for the version (after stripping a `v` prefix
if present).

The action outputs two variables: `version` and `git_hash` that you can use in the
rest of your job/workflow.

inputs: {}

outputs:
version:
value: ${{ steps.vars.outputs.version }}
description: >2
The resolved version - either a version number for tags, or 'latest' for
pull requests and pushes to the default branch.
git_hash:
value: ${{ steps.vars.outputs.git_hash }}
description: >2
The git SHA calculated by Github - for pull requests this will be the commit hash
of the merge commit.

runs:
using: composite

steps:
- name: Extract version information
id: vars
run: |
# Strip git ref prefix from version
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')

# Strip "v" prefix from tag name (if present at all)
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')

# Use Docker `latest` tag convention
[ "$VERSION" == "${{ github.event.repository.default_branch }}" ] && VERSION=latest

# PRs result in version 'merge' -> transform that into 'latest'
[ "$VERSION" == "merge" ] && VERSION=latest

echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "git_hash=${GITHUB_SHA}" >> $GITHUB_OUTPUT
shell: bash
36 changes: 36 additions & 0 deletions actions/oas-compare/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# OpenAPI comparison

Compare the generated API spec with the version tracked in the repository.

The action fails if differences are found, pointing out oversights of the author. This
requires a job or step to have completed for the generation of the API spec, see the
`oas-generate` action.

## Example usage

```yaml
env:
DJANGO_SETTINGS_MODULE: myproject.conf.ci

jobs:
generate-and-compare:
name: Generate API specification
runs-on: ubuntu-latest

steps:
- uses: maykinmedia/open-api-workflows/actions/oas-generate@main
id: generate
with:
working-directory: backend
artifact-name: openapi-yaml
schema-path: src/myproject/api/openapi.yaml

- uses: maykinmedia/open-api-workflows/actions/oas-compare@main
with:
working-directory: backend
artifact-name: openapi-yaml
schema-path: src/myproject/api/openapi.yaml
```

All inputs are optional, but recommended to specify explicitly.

53 changes: 53 additions & 0 deletions actions/oas-compare/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---

name: Compare a generated OpenAPI specification against the version tracked in git.

description: >2
Check out the repository and download the generated API specification. Then, check
if there are any differences between the versioned and generated API specs.

Requires the artifact to exist, ideally generated from the `oas-generate` action.

inputs:
schema-path:
type: string
required: false
description: Location of the OAS file, relative to working-directory if specified.
default: 'src/openapi.yaml'

artifact-name:
type: string
required: false
description: Name for the artifact of the generated schema.
default: generated-oas

working-directory:
type: string
required: false
description: Specifies the working directory where commands are run.
default: ''

runs:
using: composite

steps:
- uses: actions/checkout@v4
- name: Download generated OAS
uses: actions/download-artifact@v4
with:
name: ${{ inputs.artifact-name }}

- name: Check for OAS changes
run: |
git diff --exit-code ${{ inputs.schema-path }}
shell: bash
working-directory: ${{ inputs.working-directory }}

- name: Write failure markdown
if: ${{ failure() }}
run: |
first_lineno=$(git diff -U0 ${{ inputs.schema-path }} | grep '^@@' | head -1 | sed -E 's/.*\+([0-9]+).*/\1/')
filepath="${{ inputs.working-directory != '' && format('{0}/{1}', inputs.working-directory, inputs.schema-path) || inputs.schema-path }}"
echo "::error file=$filepath,line=$first_lineno,title=Outdated::There are changes detected in the OpenAPI specification"
echo 'Please make sure to re-generate the schema locally and commit the changes.' >> $GITHUB_STEP_SUMMARY
shell: bash
Loading