From eb228c5b8296a36b05a479954a2ad429298ff647 Mon Sep 17 00:00:00 2001 From: Frank Kilcommins Date: Tue, 25 Jun 2024 14:52:06 +0100 Subject: [PATCH] Json-schema-validation (#14) * feat(json-schema-validation): validate manifest.json * feat(json-schema-validation): add dedicated commit/pr action * chore: fix file path for schema * chore: specify draft2020 for schema validation --- .../docs-as-code-pr-commit-validation.yaml | 131 ++++++++++++++++++ .github/workflows/docs-as-code.yaml | 36 +++++ README.md | 15 +- schemas/manifest.schema.json | 81 +++++++++++ 4 files changed, 256 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/docs-as-code-pr-commit-validation.yaml create mode 100644 schemas/manifest.schema.json diff --git a/.github/workflows/docs-as-code-pr-commit-validation.yaml b/.github/workflows/docs-as-code-pr-commit-validation.yaml new file mode 100644 index 0000000..5016fa8 --- /dev/null +++ b/.github/workflows/docs-as-code-pr-commit-validation.yaml @@ -0,0 +1,131 @@ +name: Docs as Code PR and Commit Validation + +on: + push: + branches: + - '*' + pull_request: + branches: + - '*' + workflow_dispatch: + inputs: + log_level: + description: 'Log level: 1=DEBUG, 2=INFO, 3=WARNING, 4=ERROR' + required: false + default: '2' # Set the default log level to INFO + skip_api_linting: + description: 'Skip the API linting job' + required: false + default: 'false' + +env: + SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }} + LOG_LEVEL: ${{ github.event.inputs.log_level }} + +jobs: + spell-check: + runs-on: ubuntu-latest + environment: Production + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: '18' + + - name: Install cspell + run: npm install -g cspell + + - name: Run cspell + run: cspell --config ./.cspell.json "./products/**/*.md" + + validate-manifests: + runs-on: ubuntu-latest + environment: Production + needs: spell-check + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: '18' + + - name: Install AJV CLI + run: npm install -g ajv-cli + + - name: Validate manifests + run: | + # source the utility script + . ./scripts/utilities.sh + + for product in ./products/*; do + if [[ -d "$product" ]]; then + product_name=${product#./products/} + manifest="./products/$product_name/manifest.json" + if [[ -f "$manifest" ]]; then + # Further actions... + log_message $INFO "Validating manifest in product: $product_name" + log_message $DEBUG "Validating manifest: $manifest" + ajv validate -s ./schemas/manifest.schema.json -d "$manifest" --spec=draft2020 + fi + fi + done + + lint-api: + runs-on: ubuntu-latest + if: github.event.inputs.skip_api_linting != 'true' + environment: Production + needs: validate-manifests + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install SwaggerHub CLI + run: npm install -g swaggerhub-cli + + - name: Iterate over product folders and validate APIs + shell: bash + run: | + # source the utility script + . ./scripts/utilities.sh + + for product in ./products/*; do + log_message $DEBUG "Product: $product" + if [[ -d "$product" ]]; then + log_message $DEBUG "Product is a directory" + product_name=${product#./products/} + log_message $DEBUG "Product name: $product_name" + manifest="./products/$product_name/manifest.json" + log_message $DEBUG "Manifest: $manifest" + if [[ -f "$manifest" ]]; then + log_message $DEBUG "Manifest is a file" + validateAPIs=$(jq -r '.productMetadata.validateAPIs' "$manifest") + if [[ "$validateAPIs" == "true" ]]; then + log_message $INFO "Validating APIs for product: $product_name" + contentMetadata=$(jq -c '.contentMetadata[] | select(.type | ascii_downcase == "apiurl")' "$manifest") + echo "$contentMetadata" | jq -c '.' | while IFS= read -r contentMetadataItem; do + slug=$(echo "$contentMetadataItem" | jq -r '.slug') + log_message $INFO "Validating API: $slug" + swaggerhub api:validate "${SWAGGERHUB_ORG_NAME}/$slug" --fail-on-critical + done + else + log_message $WARNING "API validation is not enabled for product: $product_name" + fi + else + log_message $ERROR "Manifest is not a file" + fi + else + log_message $ERROR "Product is not a directory" + fi + done + env: + SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }} + SWAGGERHUB_ORG_NAME: ${{ vars.SWAGGERHUB_ORG_NAME }} + diff --git a/.github/workflows/docs-as-code.yaml b/.github/workflows/docs-as-code.yaml index 0dace57..08262ec 100644 --- a/.github/workflows/docs-as-code.yaml +++ b/.github/workflows/docs-as-code.yaml @@ -39,10 +39,46 @@ jobs: - name: Run cspell run: cspell --config ./.cspell.json "./products/**/*.md" + validate-manifests: + runs-on: ubuntu-latest + environment: Production + needs: spell-check + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' + + - name: Install AJV CLI + run: npm install -g ajv-cli + + - name: Validate manifests + run: | + # source the utility script + . ./scripts/utilities.sh + + for product in ./products/*; do + if [[ -d "$product" ]]; then + product_name=${product#./products/} + manifest="./products/$product_name/manifest.json" + if [[ -f "$manifest" ]]; then + # Further actions... + log_message $INFO "Validating manifest in product: $product_name" + log_message $DEBUG "Validating manifest: $manifest" + ajv validate -s ./schemas/manifest-schema.json -d "$manifest" --spec=draft2020 + fi + fi + done + lint-api: runs-on: ubuntu-latest if: github.event.inputs.skip_api_linting != 'true' environment: Production + needs: validate-manifests steps: - name: Checkout repository diff --git a/README.md b/README.md index f8dadf9..bb9ae79 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,11 @@ The following product structure must be adhered to allow for the automation to p - product One _folder_ - contains all data relevant to "Product One" - *.md _files_ - contains the markdown documents to be published within "Product One". The file name is used as the table-of-contents entry name. - images _folder_ - a folder to house the product logo and _embedded_ sub-folder - - *.png / *.jpeg - a root level image to be used as the product logo (needs to be reference from the `manifest.json`) + - `*.png / *.jpeg` - a root level image to be used as the product logo (needs to be reference from the `manifest.json`) - embedded _folder_ - a folder to storing all images to be embedded within the product markdown pages. See [Image Embedding Conventions](#image-embedding-convention) for more info on how to reference. - manifest.json - stores product metadata (like description, slug, logo url, visibility, etc.) and content metadata (like table of contents order, page nesting, etc.) - - product Two ... - - product N ... + - product Two _folder_ ... + - product N _folder_ ... ### Image Embedding Convention @@ -63,7 +63,7 @@ Check out the products operation at [`/products`](https://frankkilcommins.portal ### Table of Contents Conventions -The table of contents is completely driven by the `manifest.json` file contained within each Product folder. +The table of contents is completely driven by the `manifest.json` file contained within each Product folder. The specified manifest file MUST validate against [mainfest.schema.json](./schemas/manifest.schema.json). A sample manifest is as follows: @@ -152,7 +152,7 @@ The `contentMetadata` defines the following properties: ## GitHub Action -This repo comes with a simple boilerplate action that can be trigger manually or upon merge into the `main` branch. +This repo comes with a simple boilerplate action that can be triggered manually or upon merge into the `main` branch. The action requires the following **repository secrets** to be configured: - `SWAGGERHUB-API-KEY` - an API key associated to a user with the appropriate permission to be able to publish Portal content. See [Portal User Management](https://support.smartbear.com/swaggerhub-portal/docs/en/user-management.html) for more info. @@ -166,5 +166,6 @@ The action requires the following **repository environment variables** to be con The action performs the following jobs: 1. `spell-check`: Performs spell checking on all of the markdown files under the _products_ folder (**note** to add a list of known good custom words update the ./custom-words.txt file) -2. `lint-api`: Performs API standardization checks against each API referenced by a product manifest.json file. There is the ability to skip API validation for a specific API product via the productMetadata in the manifest.json. -3. `publish`: Publishes all of configured products into the referenced SwaggerHub Portal instance. \ No newline at end of file +2. `validate-manifests`: Performs a JSON Schema validation check against the defined product manifest.json files to ensure they are correctly specified. +3. `lint-api`: Performs API standardization checks against each API referenced by a product manifest.json file. There is the ability to skip API validation for a specific API product via the productMetadata in the manifest.json. +4. `publish`: Publishes all of configured products into the referenced SwaggerHub Portal instance. \ No newline at end of file diff --git a/schemas/manifest.schema.json b/schemas/manifest.schema.json new file mode 100644 index 0000000..e43d903 --- /dev/null +++ b/schemas/manifest.schema.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "productMetadata": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "logo": { + "type": "string" + }, + "logoDark": { + "type": "string" + }, + "autoPublish": { + "type": "boolean" + }, + "validateAPIs": { + "type": "boolean" + } + }, + "required": [ + "description", + "slug", + "public", + "hidden", + "logo", + "autoPublish", + "validateAPIs" + ] + }, + "contentMetadata": { + "type": "array", + "items": { + "type": "object", + "properties": { + "order": { + "type": "integer" + }, + "parent": { + "type": "string" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "type": { + "type": "string" + }, + "contentUrl": { + "type": "string" + } + }, + "required": [ + "order", + "name", + "slug", + "type", + "contentUrl" + ] + } + } + }, + "required": [ + "productMetadata", + "contentMetadata" + ] + } \ No newline at end of file