From c8b8d3e1a461084962d7edf9042c6d975d240b60 Mon Sep 17 00:00:00 2001 From: Frank Kilcommins Date: Mon, 24 Jun 2024 17:50:55 +0100 Subject: [PATCH] Action-quality-gates (#13) * feat(quality-gates): Add spell checking job * feat(quality-gates): add api linting job * chore: update node version * chore: rename cspell config file * chore: fix typos and extend custom-words * chore: fix typos and exclusions * chore: adjust job execution logic for linting skip * chore: change how key is passed to SHUB CLI * chore: jq parse issue * chore: change file endlines to LF * chore: set debug * chore: modify script to deal with jq parsing * chore: set debug for action output * chore: add environment * feat(quality-gates): create utilities script * chore: log message level * feat(quality-gates): update ReadMe with job info --- .cspell.json | 19 ++++ .github/workflows/docs-as-code.yaml | 77 +++++++++++++ README.md | 14 ++- custom-words.txt | 14 +++ products/Adopt a Pet/manifest.json | 105 +++++++++--------- .../Manage-Portal-Settings.md | 6 +- products/SwaggerHub Portal APIs/manifest.json | 5 +- products/Zephyr Squad APIs/manifest.json | 5 +- scripts/publish-portal-content.sh | 30 +---- scripts/utilities.sh | 32 ++++++ 10 files changed, 217 insertions(+), 90 deletions(-) create mode 100644 .cspell.json create mode 100644 custom-words.txt create mode 100644 scripts/utilities.sh diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 0000000..9751423 --- /dev/null +++ b/.cspell.json @@ -0,0 +1,19 @@ +{ + "version": "0.2", + "language": "en", + "dictionaries": [ + "custom-words" + ], + "dictionaryDefinitions": [ + { + "name": "custom-words", + "path": "./custom-words.txt", + "description": "Custom dictionary for specific words that are common and correct within our Portal documentation" + } + ], + "ignorePaths": [ + "**/node_modules/**", + "**/dist/**" + ] + } + \ No newline at end of file diff --git a/.github/workflows/docs-as-code.yaml b/.github/workflows/docs-as-code.yaml index 079ff69..0dace57 100644 --- a/.github/workflows/docs-as-code.yaml +++ b/.github/workflows/docs-as-code.yaml @@ -10,15 +10,92 @@ on: 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" + + lint-api: + runs-on: ubuntu-latest + if: github.event.inputs.skip_api_linting != 'true' + environment: Production + + 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 }} + + publish: runs-on: ubuntu-latest environment: Production + needs: [spell-check, lint-api] + if: github.event.inputs.skip_api_linting == 'true' || (needs.spell-check.result == 'success' && needs['lint-api'].result == 'success') steps: - name: Checkout repository diff --git a/README.md b/README.md index 6e72d21..f8dadf9 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,8 @@ A sample manifest is as follows: "hidden": false, "logo": "images/AdoptionsAPI.png", "logoDark": "", - "autoPublish": true + "autoPublish": true, + "validateAPIs": true }, "contentMetadata": [ { @@ -134,6 +135,7 @@ The `productMetadata` defines the following properties: | logo | The logo property specifies the path to the product logo image file. It is used to display the logo in the portal. | | logoDark | The logoDark property specifies the path to an alternative version of the product logo image file. It is used when a dark version of the logo is needed. | | autoPublish | This property determines whether the product should be automatically published after deployment or not. If set to true, the product will be published automatically. If set to false, the product will not be published automatically. | +| validateAPIs | This property determines whether API standardization rules should be ran against the API to determine conformance with organizational rules. | The `contentMetadata` defines the following properties: @@ -158,5 +160,11 @@ The action requires the following **repository secrets** to be configured: The action requires the following **repository environment** to be configured: - `Production` - the default environment. Feel free to configure additional environment and adjust the action as required if applicable for your use case. -The action requires the following **repository environment variable** to be configured: -- `SWAGGERHUB_PORTAL_SUBDOMAIN` - the sub-domain used by your portal \ No newline at end of file +The action requires the following **repository environment variables** to be configured: +- `SWAGGERHUB_PORTAL_SUBDOMAIN` - the sub-domain used by your portal +- `SWAGGERHUB_ORG_NAME` - the SwaggerHub organization housing the APIs and standardization rules + +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 diff --git a/custom-words.txt b/custom-words.txt new file mode 100644 index 0000000..0e27273 --- /dev/null +++ b/custom-words.txt @@ -0,0 +1,14 @@ +APIKEY +Arazzo +DevRel +fontname +Kilcommins +Lato +Merriweather +petco +petcopetstore +Petstore +Playfair +Roboto +SmartBear +SWAGGERHUB \ No newline at end of file diff --git a/products/Adopt a Pet/manifest.json b/products/Adopt a Pet/manifest.json index eeec555..de60ce0 100644 --- a/products/Adopt a Pet/manifest.json +++ b/products/Adopt a Pet/manifest.json @@ -1,53 +1,54 @@ -{ - "productMetadata": { - "description": "This product gives the ability to programmatically embedded a Pet adoption workflow into your application 🐶", - "slug": "pet-adoptions", - "public": true, - "hidden": false, - "logo": "images/AdoptionsAPI.png", - "logoDark": "", - "autoPublish": true - }, - "contentMetadata": [ - { - "order": 0, - "parent": "", - "name": "Getting Started with Pet Adoptions", - "slug": "getting-started-with-pet-adoptions", - "type": "markdown", - "contentUrl": "Getting-Started-With-Pet-Adoptions.md" - }, - { - "order": 1, - "parent": "getting-started-with-pet-adoptions", - "name": "Client Code (C#)", - "slug": "client-code-csharp", - "type": "markdown", - "contentUrl": "Client-Code-Csharp.md" - }, - { - "order": 2, - "parent": "getting-started-with-pet-adoptions", - "name": "Client Code (Typescript)", - "slug": "client-code-typescript", - "type": "markdown", - "contentUrl": "Client-Code-Typescript.md" - }, - { - "order": 5, - "parent": "", - "name": "Pets API", - "type": "apiUrl", - "slug": "pets-api", - "contentUrl": "https://api.swaggerhub.com/apis/frank-kilcommins/Pets/1.0.0/swagger.json" - }, - { - "order": 6, - "parent": "", - "name": "Adoptions API", - "type": "apiUrl", - "slug": "adoptions-api", - "contentUrl": "https://api.swaggerhub.com/apis/frank-kilcommins/Adoptions/1.0.0/swagger.json" - } - ] +{ + "productMetadata": { + "description": "This product gives the ability to programmatically embedded a Pet adoption workflow into your application 🐶", + "slug": "pet-adoptions", + "public": true, + "hidden": false, + "logo": "images/AdoptionsAPI.png", + "logoDark": "", + "autoPublish": true, + "validateAPIs": true + }, + "contentMetadata": [ + { + "order": 0, + "parent": "", + "name": "Getting Started with Pet Adoptions", + "slug": "getting-started-with-pet-adoptions", + "type": "markdown", + "contentUrl": "Getting-Started-With-Pet-Adoptions.md" + }, + { + "order": 1, + "parent": "getting-started-with-pet-adoptions", + "name": "Client Code (C#)", + "slug": "client-code-csharp", + "type": "markdown", + "contentUrl": "Client-Code-Csharp.md" + }, + { + "order": 2, + "parent": "getting-started-with-pet-adoptions", + "name": "Client Code (Typescript)", + "slug": "client-code-typescript", + "type": "markdown", + "contentUrl": "Client-Code-Typescript.md" + }, + { + "order": 5, + "parent": "", + "name": "Pets API", + "type": "apiUrl", + "slug": "Pets", + "contentUrl": "https://api.swaggerhub.com/apis/frank-kilcommins/Pets/1.0.0/swagger.json" + }, + { + "order": 6, + "parent": "", + "name": "Adoptions API", + "type": "apiUrl", + "slug": "Adoptions", + "contentUrl": "https://api.swaggerhub.com/apis/frank-kilcommins/Adoptions/1.0.0/swagger.json" + } + ] } \ No newline at end of file diff --git a/products/SwaggerHub Portal APIs/Manage-Portal-Settings.md b/products/SwaggerHub Portal APIs/Manage-Portal-Settings.md index 61ed600..7b3fb84 100644 --- a/products/SwaggerHub Portal APIs/Manage-Portal-Settings.md +++ b/products/SwaggerHub Portal APIs/Manage-Portal-Settings.md @@ -12,10 +12,10 @@ The following table outlines the settings per level | branding | `faviconId` | A reference to the favicon attachment. *Note* A *favicon* must be 16x16 pixels, have a `.ico`, `.gif`, or `.png` file format, and be less than 5MB in size. | | branding | `logoId` | A reference to the logo attachment. *Note* A *logo* must be 64x64 pixels, have a `.jpg`, `.gif`, or `.png` file format, and be less than 5MB in size. | | branding | `fontName` | The font to use for the portal landing page and for all product pages. Support fonts are `Open Sans`, `Montserrat`, `Roboto`, `Playfair Display`, `Lato`, or `Merriweather`. | -| branding | `accentColor` | A hexidecimal color value to use for the accent color of the landing page and all product pages. | +| branding | `accentColor` | A hexadecimal color value to use for the accent color of the landing page and all product pages. | | landingPage | `heroImageId` | A reference to the hero image attachment. *Note* A *hero* image can be up to 566 x 80 pixels, have a `.jpg`, or `.png` file format, and be less than 5MB in size. | | landingPage | `illustrationImageId` | A reference to the illustration image attachment. *Note* An *illustration* image should be at least 566 x 320 pixels, with a 16:9 aspect ratio, have a `.jpg`, or `.png` file format, and be less than 5MB in size. | -| landingPage | `pageDescription` | A short description for the portal landing page (upto 500 characters long). Markdown format supported. | +| landingPage | `pageDescription` | A short description for the portal landing page (up to 500 characters long). Markdown format supported. | ## Retrieve the base information of your portal @@ -83,7 +83,7 @@ Below is a sample *request body* which can be supplied with the request. }, "branding": { "fontName": "", - "accentColor": "" + "accentColor": "" } } ``` diff --git a/products/SwaggerHub Portal APIs/manifest.json b/products/SwaggerHub Portal APIs/manifest.json index cde30cc..1393b7e 100644 --- a/products/SwaggerHub Portal APIs/manifest.json +++ b/products/SwaggerHub Portal APIs/manifest.json @@ -6,7 +6,8 @@ "hidden": false, "logo": "images/SwaggerHub.png", "logoDark": "", - "autoPublish": true + "autoPublish": true, + "validateAPIs": true }, "contentMetadata": [ { @@ -63,7 +64,7 @@ "name": "SwaggerHub Portal API", "slug":"swaggerhub-portal-api", "type": "apiUrl", - "contentUrl": "https://api.swaggerhub.com/apis/smartbear-public/swaggerhub-portal-api/0.2.0-beta/swagger.json" + "contentUrl": "https://api.swaggerhub.com/apis/frank-kilcommins/swaggerhub-portal-api/0.2.0-beta/swagger.json" }, { "order": 7, diff --git a/products/Zephyr Squad APIs/manifest.json b/products/Zephyr Squad APIs/manifest.json index 6fcc51b..68b4e7c 100644 --- a/products/Zephyr Squad APIs/manifest.json +++ b/products/Zephyr Squad APIs/manifest.json @@ -6,7 +6,8 @@ "hidden": false, "logo": "images/Zephyr-Squad.png", "logoDark": "", - "autoPublish": true + "autoPublish": true, + "validateAPIs": false }, "contentMetadata": [ { @@ -31,7 +32,7 @@ "name": "Zephyr Squad Cloud API", "slug":"zephyr-squad-cloud-api", "type": "apiUrl", - "contentUrl": "https://api.swaggerhub.com/apis/smartbear-public/zephyr-squad-cloud-api/2.4/swagger.json" + "contentUrl": "https://api.swaggerhub.com/apis/frank-kilcommins/zephyr-squad-cloud-api/2.4/swagger.json" } ] } \ No newline at end of file diff --git a/scripts/publish-portal-content.sh b/scripts/publish-portal-content.sh index 1623dc8..833ba87 100755 --- a/scripts/publish-portal-content.sh +++ b/scripts/publish-portal-content.sh @@ -10,16 +10,8 @@ # - SWAGGERHUB_API_KEY: SwaggerHub API Key # - SWAGGERHUB_PORTAL_SUBDOMAIN: SwaggerHub Portal subdomain -#trap 'echo "Error on line $LINENO"' ERR - -# Log levels -DEBUG=1 -INFO=2 -WARNING=3 -ERROR=4 - -# Default log level -LOG_LEVEL=${LOG_LEVEL:=$INFO} +# Source the utility script to use its functions +source ./utilities.sh PORTAL_SUBDOMAIN="${SWAGGERHUB_PORTAL_SUBDOMAIN}" SWAGGERHUB_API_KEY="${SWAGGERHUB_API_KEY}" @@ -30,24 +22,6 @@ declare -g product_id declare -g document_id ## HELPER FUNCTIONS -log_message() { - local log_level=$1 - shift - local message="$@" - - case $log_level in - $DEBUG) - ([ $LOG_LEVEL -le $DEBUG ] && echo "$(date '+%Y-%m-%d %H:%M:%S') [DEBUG] $message") || true ;; - $INFO) - ([ $LOG_LEVEL -le $INFO ] && echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] $message") || true ;; - $WARNING) - ([ $LOG_LEVEL -le $WARNING ] && echo "$(date '+%Y-%m-%d %H:%M:%S') [WARNING] $message") || true ;; - $ERROR) - ([ $LOG_LEVEL -le $ERROR ] && echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $message" >&2) || true ;; - *) - echo "$(date '+%Y-%m-%d %H:%M:%S') [UNKNOWN] $message" ;; - esac -} function publish_response_check() { local response=$1 diff --git a/scripts/utilities.sh b/scripts/utilities.sh new file mode 100644 index 0000000..afb2e83 --- /dev/null +++ b/scripts/utilities.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +# Author: @frankkilcommins +# The script is used to define utility functions that are used by the other scripts in the workflow. + +# Log levels +DEBUG=1 +INFO=2 +WARNING=3 +ERROR=4 + +# Default log level +LOG_LEVEL=${LOG_LEVEL:=$INFO} + +log_message() { + local log_level=$1 + shift + local message="$@" + + case $log_level in + $DEBUG) + ([ $LOG_LEVEL -le $DEBUG ] && echo "$(date '+%Y-%m-%d %H:%M:%S') [DEBUG] $message") || true ;; + $INFO) + ([ $LOG_LEVEL -le $INFO ] && echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] $message") || true ;; + $WARNING) + ([ $LOG_LEVEL -le $WARNING ] && echo "$(date '+%Y-%m-%d %H:%M:%S') [WARNING] $message") || true ;; + $ERROR) + ([ $LOG_LEVEL -le $ERROR ] && echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] $message" >&2) || true ;; + *) + echo "$(date '+%Y-%m-%d %H:%M:%S') [UNKNOWN] $message" ;; + esac +} \ No newline at end of file