This repository demonstrates how to automate the deployment of products and product content to be published to a SwaggerHub Portal instance 🚀
This repository provides a docs-as-code approach to managing your SwaggerHub Portal products and content. By structuring your product documentation, images, and metadata in a specific folder and file structure, you can leverage automated processes (via GitHub Actions) to deploy and publish this content to your SwaggerHub Portal instance.
To leverage this automation process for your own SwaggerHub Portal, the recommended process is as follows:
- Create the appropriate folders and content underneath the products folder following the conventions laid out below.
- Setup the required repository secrets, environment(s), and environment variables needed by the configured GitHub Actions.
- Run the GitHub Actions to validate and publish your content.
The automation works by uploading content structured in accordance with the following conventions.
The following product structure must be adhered to allow for the automation to process the products and content:
- products folder- a folder containing all products to be uploaded and published. Each product sub-folder name will be used as the
product namecreated/modified within the portal- product One folder - contains all data relevant to "Product One"
*.md / *.htmlfiles - contains the markdown and/or HTML 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 themanifest.json)- embedded folder - a folder to storing all images to be embedded within the product markdown (or HTML) pages. See the image embedding conventions (markdown or html) 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 folder ...
- product N folder ...
- product One folder - contains all data relevant to "Product One"
The automation process will first publish all product images and then use the published metadata to replace image link placeholders within the markdown content.
The convention is as follows:

An example of how an image should be referenced in the markdown is as follows:
The automation process will first publish all product images and then use the published metadata to replace image link placeholders within the html content.
The convention is as follows (within an <img> element):
src="relative path to image in the ./images/embedded folder"
An example of how an image should be referenced in the HTML is as follows (you can choose appropriate dimensions and styles):
<img height="256" src="./images/embedded/arazzo_horizontal_color.png" style="aspect-ratio:993/256;" width="993"/>It's also supported to have multiple sources within a picture element:
srcset="relative path to image in the ./images/embedded folder"
An example of using a picture source to control dark mode image in the HTML is as follows:
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./images/embedded/BugSnag-Repository-Header-Dark.svg">
<img height="256" src="./images/embedded/BugSnag-Repository-Header-Light.svg" alt="SmartBear BugSnag Logo">
</picture>The OpenAPI references are predictable based on your known SwaggerHub Portal sub-domain.
The convention for embedding a link to a specific OpenAPI operation within a markdown page is as follows:
[link text of your choice](https://<YOUR-SWAGGERHUB-SUBDOMAIN>.portal.swaggerhub.com/<PRODUCT-SLUG>/docs/<API-SLUG>#/<OPENAPI-TAG-CONTAINING-OPERATIONAL-IF-APPLICABLE>/<OPENAPI-OPERATION-ID>).
So you will replace the following parameters which are all known up front:
YOUR-SWAGGERHUB-SUBDOMAIN- the sub-domain used by your portal instancePRODUCT-SLUG- the slug defined for your product (defined in the relevant manifest.json withinproductMetadata)API-SLUG- the slug defined for your API content (defined in the relevant manifest.json)OPENAPI-TAG-CONTAINING-OPERATIONAL-IF-APPLICABLE- if your operation is nested under a tag then you should define the link with the appropriate tag nameOPENAPI-OPERATION-ID- theoperationIdof the path item object within the API you want to reference from the markdown
An example of how an operation reference should be embedded in the markdown is as follows:
Check out the products operation at [`/products`](https://frankkilcommins.portal.swaggerhub.com/swaggerhub-portal/docs/swaggerhub-portal-api#/Products/createProduct).The OpenAPI references are predictable based on your known SwaggerHub Portal sub-domain.
The convention for embedding a link to a specific OpenAPI operation within a html page is as follows:
<a href="/<PRODUCT-SLUG>/docs/<API-SLUG>#/<OPENAPI-TAG-CONTAINING-OPERATIONAL-IF-APPLICABLE>/<OPENAPI-OPERATION-ID>">
So you will replace the following parameters which are all known up front:
PRODUCT-SLUG- the slug defined for your product (defined in the relevant manifest.json withinproductMetadata)API-SLUG- the slug defined for your API content (defined in the relevant manifest.json)OPENAPI-TAG-CONTAINING-OPERATIONAL-IF-APPLICABLE- if your operation is nested under a tag then you should define the link with the appropriate tag nameOPENAPI-OPERATION-ID- theoperationIdof the path item object within the API you want to reference from the markdown
An example of how an operation reference should be embedded in the HTML is as follows:
Check out the <a href="/pet-adoptions/docs/adoptions#/adoptions/postAdoption">docs</a>Linking between pages that are part of your product also follow a predictable convention. The convention for embedding a link to a specific another product page within a markdown page is as follows:
[link text of your choice](https://<YOUR-SWAGGERHUB-SUBDOMAIN>.portal.swaggerhub.com/<PRODUCT-SLUG>/docs/<CONTENTMETADATA-SLUG>).
So you will replace the following parameters which are all known up front:
YOUR-SWAGGERHUB-SUBDOMAIN- the sub-domain used by your portal instancePRODUCT-SLUG- the slug defined for your product (defined in the relevant manifest.json withinproductMetadata)CONTENTMETADATA-SLUG- the slug defined for page you want to link to (defined in the relevant manifest.json)
Here is an example of how a relative page link should be inserted into the markdown:
Check out our [csharp](https://frankkilcommins.portal.swaggerhub.com/pet-adoptions/docs/client-code-csharp) client implementation.You can link to a specific anchor using normal anchor referencing (e.g. /#someanchor being appended to your link URL). Links to external pages and sources operation as normal (e.g. [link text of your choice](full URL to external source)).
Linking between pages that are part of your product also follow a predictable convention. The convention for embedding a link to a specific another product page within a html page is as follows:
<a href="CONTENTMETADATA-SLUG">.
So you will replace the following parameters which are all known up front:
CONTENTMETADATA-SLUG- the slug defined for page you want to link to (defined in the relevant manifest.json)
Here is an example of how a relative page link should be inserted into the HTML:
<p>Check out <a href="authentication">auth docs</a>.</p>You can link to a specific anchor using normal anchor referencing (e.g. /#someanchor being appended to your link URL). Links to external pages and sources operation as normal (e.g. [link text of your choice](full URL to external source)).
When creating a HTML page, you just create a HTML snippet. All this means is that you omit the HTML, head, and body wrapper elements (<html><head>...</head><body>...</body></html>).
Ensure that your HTML conforms to the safe attributes defined in the sanitize-html package. See here for more info.
| Language | css class |
|---|---|
| plain text | language-plaintext |
| C | language-c |
| C# | language-cs |
| C++ | language-cpp |
| CSS | language-css |
| Diff | language-diff |
| HTML | language-html |
| Java | language-java |
| JavaScript | language-javascript |
| PHP | language-php |
| Python | language-python |
| Ruby | language-ruby |
| TypeScript | language-typescript |
| XML | language-xml |
An embedded snippet example for C#:
<pre><code class="language-cs">
using System.Net.Http.Headers;
var client = new HttpClient();
var request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("https://virtserver.swaggerhub.com/frank-kilcommins/Portal-API-Copy/0.2.0-beta/portals"),
Headers =
{
{ "Accept", "application/json, application/problem+json" },
{ "Authorization", "Bearer <INSERT TOKEN HERE>" },
},
};
using (var response = await client.SendAsync(request))
{
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadAsStringAsync();
Console.WriteLine(body);
}
</code></pre>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.
A sample manifest is as follows:
{
"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-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"
}
]
}The productMetadata defines the following properties:
| Property | Description |
|---|---|
| description | This property provides a description of the product. It explains what the product does and its purpose. |
| slug | The slug is a unique identifier for the product. It is used in the URL and helps to identify the product in the portal. |
| public | This property determines whether the product is publicly accessible or not. If set to true, the product can be accessed by anyone. If set to false, only authorized users can access the product. |
| hidden | The hidden property determines whether the product is visible in the portal or not. If set to true, the product will be hidden from the portal. If set to false, the product will be visible. |
| 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:
| Property | Description |
|---|---|
| order | The order in which the content should appear in the table of contents. |
| parent | The slug of the parent content, if the current item is to be nested under a parent item. |
| name | The name of the content page. |
| slug | The slug is a unique identifier for the content. It is used in the URL and helps to identify the content in the portal. |
| type | The type of the content. It can be either "markdown", or "apiUrl". For html content, you must still use markdown as the specified type. |
| contentUrl | The URL or file path of the content. For markdown/html content, it is the path to the markdown/html file. For API content, it is the URL of the Swagger/OpenAPI specification as published in SwaggerHub |
This repository provides a reusable GitHub Action for automating the deployment and publishing of products and content to a SwaggerHub Portal instance.
You can use this action in your workflows by referencing smartbear-devrel/SwaggerHub-Portal-Management@main.
Example:
jobs:
publish-portal-content:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Publish to SwaggerHub Portal
uses: smartbear-devrel/SwaggerHub-Portal-Management@main
with:
swaggerhub-api-key: ${{ secrets.SWAGGERHUB_API_KEY }}
swaggerhub-portal-subdomain: ${{ secrets.SWAGGERHUB_PORTAL_SUBDOMAIN }}
swaggerhub-org-name: ${{ secrets.SWAGGERHUB_ORG_NAME }}
log-level: '2'
publish: true| Name | Description | Required | Example |
|---|---|---|---|
| swaggerhub-api-key | SwaggerHub API Key with permissions to publish Portal content | Yes | ${{ secrets.SWAGGERHUB_API_KEY }} |
| swaggerhub-portal-subdomain | SwaggerHub Portal subdomain (e.g. mycompany for mycompany.portal.swaggerhub.com) | Yes | mycompany |
| swaggerhub-org-name | SwaggerHub organization name | Yes | my-org |
| log-level | Log level for script output (1=DEBUG, 2=INFO, 3=WARNING, 4=ERROR) | No | 2 |
| publish | Whether to publish the products after deployment (true/false) | No | true |
| skip-api-linting | Whether to skip API standardization linting (true/false) | No | false |
| skip-spell-check | Whether to skip spell checking (true/false) | No | false |
| product-folder | Path to the products folder | No | products |
| custom-words-file | Path to the custom words file for spell checking | No | ./custom-words.txt |
- 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 for more info.SWAGGERHUB_PORTAL_SUBDOMAIN- the sub-domain used by your portalSWAGGERHUB_ORG_NAME- the SwaggerHub organization housing the APIs and standardization rules
This action will:
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)validate-manifests: Performs a JSON Schema validation check against the defined product manifest.json files to ensure they are correctly specified.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.publish: Publishes all of configured products into the referenced SwaggerHub Portal instance.
