diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c2a76d4..dd21d6e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -66,7 +66,7 @@ jobs: with: subcommand: convert file: tests/fixtures/valid-v3_0.yaml - to: v3_2 + to: v3.2 output-file: converted.yaml - name: assert converted output looks like v3.2 @@ -83,7 +83,7 @@ jobs: with: subcommand: convert file: tests/fixtures/valid-v3_0.yaml - to: v3_2 + to: v3.2 merge: tests/fixtures/overlay-v3_0.yaml merge-options: deep-merge-object-schemas collapse: 'true' @@ -96,6 +96,54 @@ jobs: grep -q '/pong:' merged.yaml || { echo "merged.yaml missing /pong path from overlay"; cat merged.yaml; exit 1; } echo "OK: merge combined base and overlay paths" + - name: overlay validate + uses: ./ + with: + subcommand: overlay validate + file: tests/fixtures/overlay-doc-v1_0.yaml + + - name: overlay apply to spec + uses: ./ + with: + subcommand: overlay apply + file: tests/fixtures/valid-v3_0.yaml + overlay: tests/fixtures/overlay-doc-v1_0.yaml + output-file: overlaid.yaml + + - name: assert overlay injected the description + run: | + test -s overlaid.yaml || { echo "::error::overlaid.yaml is empty or missing"; exit 1; } + grep -q 'Added by an overlay document' overlaid.yaml || { + echo "::error::overlaid.yaml missing the overlay-injected description" + cat overlaid.yaml + exit 1 + } + echo "OK: overlay apply injected the description" + + - name: convert with apply overlay + uses: ./ + with: + subcommand: convert + file: tests/fixtures/valid-v3_0.yaml + to: v3.2 + apply: tests/fixtures/overlay-doc-v1_0.yaml + output-file: converted-overlaid.yaml + + - name: assert convert --apply output is v3.2 with the description + run: | + test -s converted-overlaid.yaml || { echo "::error::converted-overlaid.yaml is empty or missing"; exit 1; } + grep -q '^openapi: *3\.2' converted-overlaid.yaml || { + echo "::error::converted-overlaid.yaml does not declare openapi 3.2" + cat converted-overlaid.yaml + exit 1 + } + grep -q 'Added by an overlay document' converted-overlaid.yaml || { + echo "::error::converted-overlaid.yaml missing the overlay-injected description" + cat converted-overlaid.yaml + exit 1 + } + echo "OK: convert --apply produced v3.2 with the overlay description" + - name: convert without 'to' must fail id: convert-noto continue-on-error: true @@ -111,3 +159,19 @@ jobs: exit 1 fi echo "OK: convert correctly rejected missing 'to' input" + + - name: overlay apply without 'overlay' must fail + id: overlay-noov + continue-on-error: true + uses: ./ + with: + subcommand: overlay apply + file: tests/fixtures/valid-v3_0.yaml + + - name: assert overlay apply without 'overlay' failed + run: | + if [[ "${{ steps.overlay-noov.outcome }}" != "failure" ]]; then + echo "Expected overlay apply without 'overlay' to fail, got outcome=${{ steps.overlay-noov.outcome }}" + exit 1 + fi + echo "OK: overlay apply correctly rejected missing 'overlay' input" diff --git a/README.md b/README.md index 277720c..e289be8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # roas-action GitHub Action that runs [`roas`](https://github.com/sv-tools/roas) to validate or -convert OpenAPI specifications (Swagger 2.0, OpenAPI 3.0.x / 3.1.x / 3.2.x). +convert OpenAPI specifications (Swagger 2.0, OpenAPI 3.0.x / 3.1.x / 3.2.x) and to +validate, convert, or apply [OpenAPI Overlay](https://spec.openapis.org/overlay/latest.html) +documents (Overlay 1.0 / 1.1). The action is Docker-based and wraps the official [`ghcr.io/sv-tools/roas`](https://github.com/sv-tools/roas/pkgs/container/roas) @@ -40,7 +42,7 @@ Upconvert a spec to OpenAPI 3.2 and write the result next to the source: output-file: openapi.v3_2.yaml ``` -Layer overlay specs on top of a base via `merge`: +Layer additional specs on top of a base via `merge`: ```yaml - uses: sv-tools/roas-action@v1 @@ -54,27 +56,85 @@ Layer overlay specs on top of a base via `merge`: output-file: openapi.merged.yaml ``` +Apply OpenAPI Overlay documents while converting (`apply` runs last in the +pipeline: convert → merge → apply → collapse): + +```yaml +- uses: sv-tools/roas-action@v1 + with: + subcommand: convert + file: openapi.yaml + to: v3.2 + apply: | + overlays/add-servers.overlay.yaml + overlays/redact-internal.overlay.yaml + output-file: openapi.overlaid.yaml +``` + +### Overlay documents + +Validate an OpenAPI Overlay document: + +```yaml +- uses: sv-tools/roas-action@v1 + with: + subcommand: overlay validate + file: overlays/add-servers.overlay.yaml +``` + +Upconvert an Overlay 1.0 document to Overlay 1.1: + +```yaml +- uses: sv-tools/roas-action@v1 + with: + subcommand: overlay convert + file: overlays/add-servers.overlay.yaml + to: v1.1 + output-file: add-servers.v1_1.overlay.yaml +``` + +Apply one or more overlays to a target spec (`file` is the spec, `overlay` +lists the overlays, applied in order): + +```yaml +- uses: sv-tools/roas-action@v1 + with: + subcommand: overlay apply + file: openapi.yaml + overlay: | + overlays/add-servers.overlay.yaml + overlays/redact-internal.overlay.yaml + output-file: openapi.overlaid.yaml +``` + ## Inputs -| Name | Required | Default | Applies to | Description | -|-----------------|----------|------------|------------|-----------------------------------------------------------------------------------------------| -| `subcommand` | no | `validate` | both | `validate` or `convert`. | -| `file` | yes | — | both | Path to the OpenAPI spec (JSON or YAML), relative to the repo root. | -| `from` | no | — | both | Force the input spec version. One of `v2`, `v3.0`, `v3.1`, `v3.2`. | -| `to` | yes\* | — | convert | Target version for `convert`. Required when `subcommand: convert`. | -| `merge` | no | — | convert | Newline-separated list of overlay specs to merge on top of the base after version conversion. | -| `merge-options` | no | — | convert | Whitespace-separated merge options (requires `merge`). See [merge options](#merge-options). | -| `collapse` | no | `false` | convert | Lift inline components into the root bag and replace call sites with `$ref`s. | -| `format` | no | auto | both | Force input format: `json` or `yaml`. By default inferred from the file extension. | -| `load` | no | — | both\*\* | Whitespace-separated `$ref` loaders: `file`, `http`. On `convert` requires `collapse: true`. | -| `ignore` | no | — | validate | Whitespace-separated validation checks to skip (see [check list](#validation-checks)). | -| `print` | no | `false` | validate | If `true`, echo the parsed spec on stdout (diagnostics stay on stderr). | -| `output-format` | no | match in | convert | Force output format: `json` or `yaml`. | -| `output-file` | no | stdout | convert | Write the converted spec to this path. If unset, output streams to the action log. | +"Applies to" abbreviations: **V** = `validate`, **C** = `convert`, +**OV** = `overlay validate`, **OC** = `overlay convert`, **OA** = `overlay apply`. + +| Name | Required | Default | Applies to | Description | +|-----------------|----------|------------|----------------|----------------------------------------------------------------------------------------------------| +| `subcommand` | no | `validate` | — | `validate`, `convert`, `overlay validate`, `overlay convert`, or `overlay apply`. | +| `file` | yes | — | all | Positional input: the spec (V, C, OA) or the Overlay document (OV, OC), relative to the repo root. | +| `from` | no | — | V, C | Force the input spec version. One of `v2`, `v3.0`, `v3.1`, `v3.2`. | +| `to` | yes\* | — | C, OC | Target version. For C: `v3.0`/`v3.1`/`v3.2` etc. For OC: `v1.0`/`v1.1`. Required for both. | +| `merge` | no | — | C | Newline-separated list of specs to merge on top of the base after version conversion. | +| `merge-options` | no | — | C | Whitespace-separated merge options (requires `merge`). See [merge options](#merge-options). | +| `apply` | no | — | C | Newline-separated Overlay documents to apply after merge (before collapse). | +| `overlay` | yes\* | — | OA | Newline-separated Overlay documents to apply to the spec. Required when `subcommand: overlay apply`.| +| `apply-options` | no | — | C, OA | Whitespace-separated overlay apply options. See [overlay apply options](#overlay-apply-options). | +| `collapse` | no | `false` | C | Lift inline components into the root bag and replace call sites with `$ref`s. | +| `format` | no | auto | all | Force input format: `json` or `yaml`. By default inferred from the file extension. | +| `load` | no | — | V, C\*\* | Whitespace-separated `$ref` loaders: `file`, `http`. On `convert` requires `collapse: true`. | +| `ignore` | no | — | V, OV | Whitespace-separated checks to skip. See [validation checks](#validation-checks). | +| `print` | no | `false` | V, OV | If `true`, echo the parsed spec/overlay on stdout (diagnostics stay on stderr). | +| `output-format` | no | match in | C, OC, OA | Force output format: `json` or `yaml`. | +| `output-file` | no | stdout | all | Write command output to this path. If unset, output streams to the action log. | ### Validation checks -Values accepted by `ignore` (passed straight through to `roas validate --ignore`): +For `validate`, values accepted by `ignore` (passed straight through to +`roas validate --ignore`): ``` missing-tags, external-references, invalid-urls, non-uniq-operation-ids, @@ -86,7 +146,21 @@ empty-info-title, empty-info-version, empty-response-description, empty-external-documentation-url ``` -Run `roas validate --help` for the description of each check. +For `overlay validate`, `ignore` accepts only `empty-info-title` and +`empty-info-version`. + +Run `roas validate --help` (or `roas overlay validate --help`) for the +description of each check. + +### Overlay apply options + +Values accepted by `apply-options` (passed straight through as +`--apply-option`, for `convert` with `apply` and for `overlay apply`): + +- `error-on-zero-match` — fail when an action's `target` JSONPath selects zero + nodes. By default a zero-match action is a no-op (per the Overlay spec). +- `error-on-mixed-kind-match` — fail when an `update` action's `target` selects + a mix of objects and arrays. Normative in Overlay 1.1; this opts 1.0 in. ### Merge options diff --git a/action.yml b/action.yml index 5e5c7bb..c482a1e 100644 --- a/action.yml +++ b/action.yml @@ -1,38 +1,44 @@ name: 'Roas OpenAPI Action' -description: 'Validate and convert OpenAPI specs with roas' +description: 'Validate and convert OpenAPI specs and Overlay documents with roas' branding: icon: 'check-circle' color: 'purple' inputs: subcommand: - description: 'roas subcommand: validate or convert' + description: 'roas subcommand: validate, convert, overlay validate, overlay convert, or overlay apply' default: 'validate' file: - description: 'Path to the OpenAPI spec (JSON or YAML)' + description: 'Positional input: the OpenAPI spec (validate, convert, overlay apply) or the Overlay document (overlay validate, overlay convert), as JSON or YAML' required: true from: - description: 'Force input spec version (e.g. v2, v3_0, v3_1, v3_2)' + description: 'Force input spec version (e.g. v2, v3.0, v3.1, v3.2). Spec validate/convert only.' to: - description: 'Target version for convert (e.g. v3_1, v3_2). Required when subcommand=convert.' + description: 'Target version. For convert: a spec version (e.g. v3.1, v3.2). For overlay convert: an overlay version (v1.0, v1.1). Required for convert and overlay convert.' merge: description: 'Newline-separated list of additional specs to merge on top of the base (convert only). Each source is upconverted to the target version, then merged in incoming-order.' merge-options: description: 'Whitespace-separated merge options (convert only, requires merge): base-wins, error-on-conflict, deep-merge-object-schemas, merge-info, replace-lists-when-empty.' + apply: + description: 'Newline-separated list of Overlay documents to apply on top of the converted/merged spec (convert only). Applied in order, last in the pipeline (after merge, before collapse).' + overlay: + description: 'Newline-separated list of Overlay documents to apply to the target spec (overlay apply only). Applied in order. Required when subcommand=overlay apply.' + apply-options: + description: 'Whitespace-separated overlay apply options (convert with apply, or overlay apply): error-on-zero-match, error-on-mixed-kind-match.' collapse: description: 'Lift inline components into the root bag and replace call sites with $refs (convert only).' default: 'false' load: description: 'Whitespace-separated $ref loaders to enable: file, http' ignore: - description: 'Whitespace-separated validation checks to skip (validate only)' + description: 'Whitespace-separated checks to skip. validate: validation checks. overlay validate: empty-info-title, empty-info-version.' format: description: 'Force input format: json or yaml' output-format: - description: 'Force output format for convert: json or yaml' + description: 'Force output format for convert, overlay convert, and overlay apply: json or yaml' output-file: - description: 'Write convert output to this path instead of stdout' + description: 'Write command output to this path instead of stdout' print: - description: 'Echo parsed spec on stdout (validate only)' + description: 'Echo the parsed spec/overlay on stdout (validate and overlay validate only)' default: 'false' runs: using: 'docker' @@ -41,3 +47,4 @@ runs: INPUT_OUTPUT_FILE: ${{ inputs.output-file }} INPUT_OUTPUT_FORMAT: ${{ inputs.output-format }} INPUT_MERGE_OPTIONS: ${{ inputs.merge-options }} + INPUT_APPLY_OPTIONS: ${{ inputs.apply-options }} diff --git a/entrypoint.sh b/entrypoint.sh index 4e6913b..499251b 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,7 +3,7 @@ set -euo pipefail sub="${INPUT_SUBCOMMAND:-validate}" case "$sub" in - validate|convert) ;; + validate|convert|"overlay validate"|"overlay convert"|"overlay apply") ;; *) echo "roas-action: unknown subcommand: $sub" >&2; exit 2 ;; esac @@ -12,33 +12,68 @@ if [[ -z "${INPUT_FILE:-}" ]]; then exit 2 fi -args=("$sub") +# Seed argv with the (possibly two-word) subcommand, then append flags. +read -ra args <<< "$sub" -[[ -n "${INPUT_FROM:-}" ]] && args+=(--from "$INPUT_FROM") +# --format applies to every command (input spec / overlay). [[ -n "${INPUT_FORMAT:-}" ]] && args+=(--format "$INPUT_FORMAT") -if [[ "$sub" == "convert" ]]; then - if [[ -z "${INPUT_TO:-}" ]]; then - echo "roas-action: 'to' is required when subcommand=convert" >&2 - exit 2 - fi - args+=(--to "$INPUT_TO") - [[ -n "${INPUT_OUTPUT_FORMAT:-}" ]] && args+=(--output-format "$INPUT_OUTPUT_FORMAT") - while IFS= read -r v; do - [[ -n "$v" ]] && args+=(--merge "$v") - done <<< "${INPUT_MERGE:-}" - for v in ${INPUT_MERGE_OPTIONS:-}; do args+=(--merge-option "$v"); done - if [[ "${INPUT_COLLAPSE:-false}" == "true" ]]; then - args+=(--collapse) - for v in ${INPUT_LOAD:-}; do args+=(--load "$v"); done - fi -fi +case "$sub" in + validate) + [[ -n "${INPUT_FROM:-}" ]] && args+=(--from "$INPUT_FROM") + for v in ${INPUT_LOAD:-}; do args+=(--load "$v"); done + for v in ${INPUT_IGNORE:-}; do args+=(--ignore "$v"); done + [[ "${INPUT_PRINT:-false}" == "true" ]] && args+=(--print) + ;; -if [[ "$sub" == "validate" ]]; then - for v in ${INPUT_LOAD:-}; do args+=(--load "$v"); done - for v in ${INPUT_IGNORE:-}; do args+=(--ignore "$v"); done - [[ "${INPUT_PRINT:-false}" == "true" ]] && args+=(--print) -fi + convert) + if [[ -z "${INPUT_TO:-}" ]]; then + echo "roas-action: 'to' is required when subcommand=convert" >&2 + exit 2 + fi + args+=(--to "$INPUT_TO") + [[ -n "${INPUT_FROM:-}" ]] && args+=(--from "$INPUT_FROM") + [[ -n "${INPUT_OUTPUT_FORMAT:-}" ]] && args+=(--output-format "$INPUT_OUTPUT_FORMAT") + while IFS= read -r v; do + [[ -n "$v" ]] && args+=(--merge "$v") + done <<< "${INPUT_MERGE:-}" + for v in ${INPUT_MERGE_OPTIONS:-}; do args+=(--merge-option "$v"); done + while IFS= read -r v; do + [[ -n "$v" ]] && args+=(--apply "$v") + done <<< "${INPUT_APPLY:-}" + for v in ${INPUT_APPLY_OPTIONS:-}; do args+=(--apply-option "$v"); done + if [[ "${INPUT_COLLAPSE:-false}" == "true" ]]; then + args+=(--collapse) + for v in ${INPUT_LOAD:-}; do args+=(--load "$v"); done + fi + ;; + + "overlay validate") + for v in ${INPUT_IGNORE:-}; do args+=(--ignore "$v"); done + [[ "${INPUT_PRINT:-false}" == "true" ]] && args+=(--print) + ;; + + "overlay convert") + if [[ -z "${INPUT_TO:-}" ]]; then + echo "roas-action: 'to' is required when subcommand='overlay convert'" >&2 + exit 2 + fi + args+=(--to "$INPUT_TO") + [[ -n "${INPUT_OUTPUT_FORMAT:-}" ]] && args+=(--output-format "$INPUT_OUTPUT_FORMAT") + ;; + + "overlay apply") + if [[ -z "${INPUT_OVERLAY:-}" ]]; then + echo "roas-action: 'overlay' is required when subcommand='overlay apply'" >&2 + exit 2 + fi + while IFS= read -r v; do + [[ -n "$v" ]] && args+=(--overlay "$v") + done <<< "${INPUT_OVERLAY:-}" + for v in ${INPUT_APPLY_OPTIONS:-}; do args+=(--apply-option "$v"); done + [[ -n "${INPUT_OUTPUT_FORMAT:-}" ]] && args+=(--output-format "$INPUT_OUTPUT_FORMAT") + ;; +esac args+=("$INPUT_FILE") @@ -46,4 +81,4 @@ if [[ -n "${INPUT_OUTPUT_FILE:-}" ]]; then exec /usr/local/bin/roas "${args[@]}" > "$INPUT_OUTPUT_FILE" else exec /usr/local/bin/roas "${args[@]}" -fi +fi \ No newline at end of file diff --git a/tests/fixtures/overlay-doc-v1_0.yaml b/tests/fixtures/overlay-doc-v1_0.yaml new file mode 100644 index 0000000..47d4860 --- /dev/null +++ b/tests/fixtures/overlay-doc-v1_0.yaml @@ -0,0 +1,8 @@ +overlay: 1.0.0 +info: + title: Add a description + version: 1.0.0 +actions: + - target: $.info + update: + description: Added by an overlay document.