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
18 changes: 18 additions & 0 deletions .github/workflows/test-openapi-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ jobs:
- fixture: not-openapi.yaml
fail-on-warn: 'false'
expect: failure
- fixture: invalid-json.json
fail-on-warn: 'false'
expect: failure
- fixture: convention-warnings.yaml
fail-on-warn: 'false'
expect: success
Expand Down Expand Up @@ -94,3 +97,18 @@ jobs:
exit 1
fi
echo "Output counts verified: errors=${ERROR_COUNT} warnings=${WARNING_COUNT}"

- name: Verify processing error output
if: matrix.fixture == 'not-openapi.yaml' || matrix.fixture == 'invalid-json.json'
env:
ERROR_COUNT: ${{ steps.lint.outputs.error-count }}
RESULTS_FILE: ${{ steps.lint.outputs.results-file }}
run: |
if [ "${ERROR_COUNT}" != "1" ]; then
echo "::error::Expected 1 processing error, got ${ERROR_COUNT}"
exit 1
fi
if ! jq -e '.problems | any(.ruleId == "redocly-processing-error")' "${RESULTS_FILE}" > /dev/null; then
echo "::error::Expected a redocly-processing-error result"
exit 1
fi
28 changes: 14 additions & 14 deletions openapi/lint/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<!-- Generated by https://github.com/reakaleek/gh-action-readme -->
# <!--name-->Elastic OpenAPI Linter<!--/name-->
<!--description-->
Lint an OpenAPI 2.0 (Swagger), 3.0, or 3.1 spec against the centralized Elastic ruleset (Spectral)
Lint an OpenAPI 2.0 (Swagger), 3.0, or 3.1 spec against the centralized Elastic ruleset (Redocly)
<!--/description-->

## Inputs
Expand All @@ -14,11 +14,11 @@ Lint an OpenAPI 2.0 (Swagger), 3.0, or 3.1 spec against the centralized Elastic

## Outputs
<!--outputs-->
| Name | Description |
|-----------------|------------------------------------------------|
| `error-count` | Number of error-level results |
| `warning-count` | Number of warning-level results |
| `results-file` | Path to the machine-readable JSON results file |
| Name | Description |
|-----------------|---------------------------------------|
| `error-count` | Number of error-level results |
| `warning-count` | Number of warning-level results |
| `results-file` | Path to the Redocly JSON results file |
<!--/outputs-->

## Usage
Expand All @@ -36,28 +36,28 @@ steps:

## Ruleset

The ruleset is centralized in this action (`openapi/lint/ruleset.yaml`) — consumers do not author
The ruleset is centralized in this action (`openapi/lint/redocly.yaml`) — consumers do not author
their own rules. Rule changes are proposed via PR to `elastic/docs-actions`.

It has two severity tiers:

- **error** — the spec parses, all `$ref`s resolve, and it conforms to the OAS schema for its
detected version (Swagger 2.0, OpenAPI 3.0.x, or OpenAPI 3.1.x). These always fail the check.
- **warn** — authoring conventions, currently the generic hygiene rules built into `spectral:oas`
- **warn** — authoring conventions, using built-in and configurable Redocly rules
(`operationId` presence, required descriptions/tags/servers, tags defined at root). These pass
by default so existing specs aren't broken; set `fail-on-warn: true` to enforce them.

Elastic-specific conventions (`operationId` casing, security scheme naming, the `x-state` shape)
aren't encoded as rules yet — they're sketched as commented-out examples in `ruleset.yaml` pending
team agreement (see the analysis in elastic/docs-eng-team#639). Land them via PR once adopted.
aren't encoded as rules yet, pending team agreement (see the analysis in
elastic/docs-eng-team#639). Land them via PR once adopted.

`spec-path` should be repo-relative — PR annotations are anchored to the path as given, so an
absolute path won't map back to a file in the diff.

This action installs Spectral via `npm ci` against a lockfile committed in this directory
(`openapi/lint/package-lock.json`), pinning the full dependency tree — not just the CLI version —
so lint results are reproducible run to run. It only calls the public npm registry and needs no
secrets, so it's safe to run on fork PRs.
This action installs Redocly CLI via `npm ci` against a lockfile committed in this directory
(`openapi/lint/package-lock.json`), pinning the CLI version and package integrity so lint results
are reproducible run to run. It only calls the public npm registry and needs no secrets, so it's
safe to run on fork PRs.

**Note:** an allowlist rule for `x-*` vendor extensions is planned as a follow-up and is not yet
part of this ruleset.
92 changes: 58 additions & 34 deletions openapi/lint/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: 'Elastic OpenAPI Linter'
description: 'Lint an OpenAPI 2.0 (Swagger), 3.0, or 3.1 spec against the centralized Elastic ruleset (Spectral)'
description: 'Lint an OpenAPI 2.0 (Swagger), 3.0, or 3.1 spec against the centralized Elastic ruleset (Redocly)'
author: 'Elastic'

branding:
Expand All @@ -23,7 +23,7 @@ outputs:
description: 'Number of warning-level results'
value: ${{ steps.lint.outputs.warning-count }}
results-file:
description: 'Path to the machine-readable JSON results file'
description: 'Path to the Redocly JSON results file'
value: ${{ steps.lint.outputs.results-file }}

runs:
Expand Down Expand Up @@ -53,13 +53,13 @@ runs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '20.19.0'

- name: Install Spectral CLI
- name: Install Redocly CLI
shell: bash
run: npm ci --prefix "${{ github.action_path }}"

- name: Run Spectral
- name: Run Redocly
id: lint
shell: bash
env:
Expand All @@ -68,37 +68,56 @@ runs:
run: |
set -euo pipefail

FAIL_SEV=error
RESULTS="${RUNNER_TEMP}/redocly-results.json"
STDERR="${RUNNER_TEMP}/redocly-stderr.log"
HAS_JSON=true
EXIT=0

if [ "${FAIL_ON_WARN}" == "true" ]; then
FAIL_SEV=warn
"${{ github.action_path }}/node_modules/.bin/redocly" lint \
--config="${{ github.action_path }}/redocly.yaml" \
--lint-config=error \
--format=json \
--max-problems=10000 \
"${SPEC_PATH}" > "${RESULTS}" 2> "${STDERR}" || EXIT=$?

# Redocly doesn't produce JSON when it cannot parse or recognize the
# document. Normalize that case to the ordinary results schema.
if [ ! -s "${RESULTS}" ] || ! jq -e '.totals and .problems' "${RESULTS}" > /dev/null 2>&1; then
HAS_JSON=false
while IFS= read -r line; do
echo "${line}"
done < "${STDERR}"
jq -n --arg ref "${SPEC_PATH}" '{
totals: {errors: 1, warnings: 0, ignored: 0},
problems: [{
ruleId: "redocly-processing-error",
severity: "error",
message: "Redocly could not process this API description. See the step log for details.",
location: [{source: {ref: $ref}, pointer: "#/"}],
suggest: []
}]
}' > "${RESULTS}"
echo "::error file=${SPEC_PATH}::Redocly could not process this API description. See the step log for details."
EXIT=1
fi

RESULTS="${RUNNER_TEMP}/spectral-results.json"
EXIT=0
ERROR_COUNT="$(jq -r '.totals.errors' "${RESULTS}")"
WARNING_COUNT="$(jq -r '.totals.warnings' "${RESULTS}")"

# JSON is needed for outputs and the summary. Run the GitHub formatter
# separately because Redocly accepts only one output format per run.
if [ "${HAS_JSON}" == "true" ]; then
"${{ github.action_path }}/node_modules/.bin/redocly" lint \
--config="${{ github.action_path }}/redocly.yaml" \
--lint-config=error \
--format=github-actions \
--max-problems=10000 \
"${SPEC_PATH}" || true
fi

"${{ github.action_path }}/node_modules/.bin/spectral" lint \
--ruleset "${{ github.action_path }}/ruleset.yaml" \
-f github-actions -f json --output.json "${RESULTS}" \
--fail-severity="${FAIL_SEV}" \
"${SPEC_PATH}" || EXIT=$?

# Diagnostic codes that always fail the check regardless of fail-on-warn:
# they mean the document isn't lintable at all, not that a rule was
# violated, so they can't be tiered via ruleset.yaml like normal rules.
# unrecognized-format is emitted by Spectral itself at warn severity.
ALWAYS_ERROR_CODES='["unrecognized-format"]'

read -r ERROR_COUNT WARNING_COUNT HAS_ALWAYS_ERROR < <(jq -r --argjson codes "${ALWAYS_ERROR_CODES}" '
[
([.[] | select(.severity == 0)] | length),
([.[] | select(.severity == 1)] | length),
(any(.[]; .code as $c | $codes | index($c) != null))
] | @tsv
' "${RESULTS}")

if [ "${HAS_ALWAYS_ERROR}" == "true" ]; then
echo "::error file=${SPEC_PATH}::Document failed a structural check (not a recognized OpenAPI 2.0/3.0/3.1 spec)"
if [ "${ERROR_COUNT}" -gt 0 ]; then
EXIT=1
elif [ "${FAIL_ON_WARN}" == "true" ] && [ "${WARNING_COUNT}" -gt 0 ]; then
EXIT=1
fi

Expand Down Expand Up @@ -126,9 +145,14 @@ runs:
echo "**Errors:** ${ERROR_COUNT} · **Warnings:** ${WARNING_COUNT}"
echo ""
if [ "$((ERROR_COUNT + WARNING_COUNT))" -gt 0 ]; then
echo "| Severity | Rule | Line | Message |"
echo "| Severity | Rule | Location | Message |"
echo "|---|---|---|---|"
jq -r '.[] | "| " + (if .severity == 0 then "error" else "warning" end) + " | " + .code + " | " + (.range.start.line + 1 | tostring) + " | " + (.message | gsub("\\|"; "\\|")) + " |"' "${RESULTS_FILE}"
jq -r '.problems[] |
"| " + .severity +
" | " + (.ruleId // "redocly") +
" | `" + (.location[0].pointer // "#/") + "`" +
" | " + (.message | gsub("[\\r\\n]+"; " ") | gsub("\\|"; "\\|")) + " |"
' "${RESULTS_FILE}"
fi
} >> "$GITHUB_STEP_SUMMARY"
fi
Expand Down
Loading
Loading