Skip to content

Commit

Permalink
Use provided registry namespace and clean up (#13)
Browse files Browse the repository at this point in the history
* use REGISTRY_NAMESPACE in package.json, update parameter names

* update README

* make generate-client script clearer

* allow user to specify repo url; clarify requirement of scoped package

* comments David

* make repository url required
  • Loading branch information
hgrsd authored Oct 15, 2020
1 parent 985274e commit 878309a
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 51 deletions.
82 changes: 58 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,84 @@
## Hatch
# Hatch

### Description
Hatch is a GitHub Action that uses [openapi-generator](https://github.com/OpenAPITools/openapi-generator) to automatically generate and publish a TypeScript-Axios API client for your service, based on your service's OpenAPI v3 spec.

Hatch is a GitHub Action that uses [openapi-generator](https://github.com/OpenAPITools/openapi-generator) to automatically generate and publish a TypeScript-axios API client for your service, based on your service's OpenAPI spec.
## Prerequisites

### Usage
In order to be able to use this Action in the CI/CD pipeline of your service, there are two prerequisites:

To use this Action, you will need:
1. Your repository must have an associated NPM package registry, and you need a read/write registry token that you can pass into the Action.
2. The Action must be able to access, online, the OpenAPI v3 spec for your service. (At Birdie, we use an earlier step of our CI/CD pipeline to deploy the relevant service, which exposes an endpoint to fetch the latest version of our OpenAPI spec. That means that when the Hatch action runs, it can access the latest version.)

- An OpenAPI v3 spec of your service, which is accessible to the Action. Easiest might be to expose an endpoint that serves your service's OpenAPI spec
- A GitHub NPM registry and an access token to publish packages to it

You can include this Action in your GitHub workflow as follows:
Note: The Hatch action currently only works with GitHub Packages, and [GitHub Packages only supports scoped packages](https://docs.github.com/en/free-pro-team@latest/packages/using-github-packages-with-your-projects-ecosystem/configuring-npm-for-use-with-github-packages#publishing-a-package).

## How to use


Once you have the prerequisites in place, you can include this Action in your GitHub workflow. The Action requires the following 5 parameters to be passed in:

| Name | Description |
| ------------------ | ------------------------------------------------------------------------------------------ |
| openapi_spec_url | The URL pointing at the service's OpenAPI v3 spec in JSON format |
| package_name | The name to use for the generated client package |
| registry_namespace | The name of the GitHub NPM registry namespace under which to publish the generated package |
| registry_token | A read/write access token for the package registry, used to publish the generated client |
| repository_url | The url of the GitHub repository in which the generated package should be published |

See below for an example configuration:

```yaml
generate-api-client:
name: Generate API client
name: Generate API client using Hatch
needs: <any prerequisite earlier job>
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2 # <-- this one is needed for hatch to access source code
- uses: actions/checkout@v2
- name: Run Hatch action
uses: birdiecare/hatch@v0.0.12 # <-- use current version
uses: birdiecare/hatch@v0.1.0
with:
path: <e.g. https://staging.myservice.com/docs.json>
name: <Name of your service, e.g. my-service> # the generated client will automatically be called my-service-client
token: <your GitHub Registry Token>
openapi_spec_url: https://my-service.io/docs.json
package_name: my-api-client
registry_namespace: @mycompany # the package will be published under @mycompany/my-api-client
registry_token: ${{ secrets.MY_REGISTRY_TOKEN }}
repository_url: ssh://[email protected]/my-organisation/my-service.git # the package will be published as part of this repo
```
Running this Action will publish an NPM package called `my-service-client` as a package **inside the repository of the service running the Action**.
### Exposing DTO classes using Hatch
At [Birdie](https://birdie.care), we use [NestJS and Swagger](https://docs.nestjs.com/openapi/introduction) to annotate the DTO classes we use for our endpoints and RPC operations. For some of our use cases, it was helpful to have these full, Swagger-annotated DTO classes available as part of the generated client package, so that other services making use of this client have access to these annotations instead of just the interfaces provided by `openapi-generator`.

### Exposing Swagger models
To support this use case, the Hatch action can scan the `src` folder of your service's git repo for files matching the `*.public-models.ts` glob. If you wish to include Swagger-annotated (or other) DTO classes in the generated client package, then make sure to name them accordingly.

If you want to create a proxy endpoint in `core-api`, you would like to import the class decorated with `@nestjs/swagger` decorators to avoid re-declaring the swagger docs.
Once included, these DTOs can be imported from the generated client:

In that case, the `birdiecare/hatch` action can scan your `src` folder of the git repo in search of all the files matching the `*.public-models.ts` glob. Other files will be ignored, so make sure you put the swagger classes in that file.

Then in the `core-api` you can import the file from `package/models/filename` nested import, e.g.:
```ts
import { IdentifierMapping } from '@birdiecare/rostering-integrations-client/models/identifier-mapping.public-models';
import { MyDTO } from '@mycompany/my-service-client/models/my-models.public-models';
```
⚠️ Note that public model files currently do not support any other imports than `@nestjs/swagger` and `class-validator`. If you import anything else, they will fail to compile. These two dependencies are peer-dependencies of the generated client package and should be installed in your consuming project if you use the public models feature.

## How does Hatch work "under the hood"?

### openapi-generator
Hatch uses openapi-generator to generate a client from your OpenAPI v3 spec. openapi-generator has many "generators" to allow users to generate different types of clients; the one used by Hatch is the [typescript-axios generator](https://openapi-generator.tech/docs/generators/typescript-axios). Hatch currently does not allow changing the type of generator used; if you wish to use Hatch for a different target you will have to fork this repo and amend it (or open a PR to allow configuring the generator used 🤩).

The version of openapi-generator used is fixed in the `generate-client.sh` script (currently using v4.3.0.).

### Building and publishing the package
Once a client is generated using openapi-generator, Hatch compares the created client (and any included public models) against the latest published version of the client from your package registry. If the client has changed (because your spec has changed), or if there is no previous version, it will continue to publish the newly generated client to your GitHub package registry.

Hatch first compiles the Typescript code so that the package can be published, using [this](templates/tsconfig.json) tsconfig, before creating an NPM package. The generated package will contain a `package.json` with basic information and the required dependencies. The template used for this can be found [here](templates/package.json.template).

The version of each published package is automatically generated based on the date of publication, following the format: `0.0.YYYYMMDDHHMMSS`.

⚠️ Beware that the public models files shouldn't have any other imports than `@nestjs/swagger` and `class-validator` ones! Otherwise (especially in case of relative imports) it gonna fail to compile.
## Troubleshooting

### Templating
While using Hatch at Birdie, it has been stable and failures have been relatively rare. Where we have seen failures, they have generally been caused by one of the following reasons:

[openapi-generator](https://github.com/OpenAPITools/openapi-generator) works by running a specified [code generator](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators.md) on an OpenAPI spec. Hatch uses [typescript-axios](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/typescript-axios.md) for this.
- The OpenAPI spec not being available at the specified URL
- The OpenAPI spec not using v3 of the spec
- Invalid Swagger annotations used in our application code, leading to an invalid OpenAPI spec which crashes openapi-generator
- A bug in the version of openapi-generator used, which was resolved after using a newer version

The code generator generates an API client based on the OpenAPI spec using [Mustache](https://mustache.github.io) templates. These templates are shipped with the relevant code generator. The `/templates/openapi-generator/typescript-axios` folder of this repo houses the templates used by this Action. They are unchanged from the standard templates used by `typescript-axios`, but are included so that it is easy to make changes if your use case requires them.
If the Action fails, make sure to look at its logs, which will tell you at which step it has failed. If none of the above issues appear to be the case, then please feel free to open an issue.
26 changes: 15 additions & 11 deletions action.yaml
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
name: 'Hatch'
description: 'Generate and publish a TypeScript-axios API client based on your OpenAPI spec'
inputs:
path:
description: 'Path to JSON-formatted OpenAPI spec'
openapi_spec_url:
description: 'URL pointing at the JSON-formatted OpenAPI v3 spec for your service'
required: true
name:
description: 'Your service name (client will be published as your-service-name-client)'
required: true
token:
description: 'Your GitHub Registry token'
package_name:
description: 'The name to use for the generated client package'
required: true
registry_namespace:
description: 'Name of your GitHub NPM registry namespace (without the "@" where applicable)'
description: 'Name of the GitHub NPM registry namespace under which you want to publish the generated package, eg "@my-namespace"'
required: true
registry_token:
description: 'Your GitHub NPM registry token'
required: true
repository_url:
description: 'The url of the GitHub repository in which the generated package should be published'
required: true
runs:
using: 'docker'
image: 'Dockerfile'
args:
- INPUT_PATH
- INPUT_NAME
- INPUT_TOKEN
- INPUT_OPENAPI_SPEC_URL
- INPUT_PACKAGE_NAME
- INPUT_REGISTRY_NAMESPACE
- INPUT_REGISTRY_TOKEN
- INPUT_REPOSITORY_URL
51 changes: 35 additions & 16 deletions generate-client.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
#!/usr/bin/env sh

# This shell script uses a specified swagger document to generate a typescript-axios API client using openapi-generator.
# It then compares this generated client to the previous version and, if it has changed, publishes it as an NPM package.
#
# This shell script runs through the following steps to generate and publish a client based on the provided OpenAPI v3 specification:
# 1. Download and install prerequisites.
# 2. Use openapi-generator to generate an API client based on the provided OpenAPI v3 spec
# 3. Crawl for any public models (named <xxx>.public-models.ts) to include in the package.
# 4. Build the new package
# 5. Fetch the current version of the package, if available.
# 6. If there are differences, or if no previous version could be fetched, publish the new version to the NPM registry

echo "Hatch action started."

# Step 1: Download and install prerequisites
echo "Downloading and installing prerequisites..."
apt-get update
apt-get -y install wget jq
Expand All @@ -17,9 +22,10 @@ then
fi
echo "[SUCCESS]"

echo "Running openapi-generator using swagger doc at $INPUT_PATH..."
# Step 2: Generate client
echo "Running openapi-generator using swagger doc at $INPUT_OPENAPI_SPEC_URL..."
java -jar openapi-generator-cli.jar generate \
--input-spec $INPUT_PATH \
--input-spec $INPUT_OPENAPI_SPEC_URL \
--generator-name typescript-axios \
--additional-properties=supportsES6=true,modelPropertyNaming=original,withInterfaces=true \
--output /client
Expand All @@ -30,14 +36,15 @@ then
fi
echo "[SUCCESS]"

# Step 3: Crawl for public models
echo "Checking GitHub repository folder..."
cd $GITHUB_WORKSPACE/src

echo "Searching for models files..."
globs=$(find . -iname '*.public-models.ts')

if [ -z "$globs" ]; then
echo "Not found any model files"
echo "No models found"
else
echo "Copying models files to /client/models"
mkdir -p /client/models
Expand All @@ -47,10 +54,22 @@ else
done
fi

# Step 4: Build client package
echo "Creating package.json from template..."
cd /client
DATE=$(date -u +"%Y%m%d%H%M%S")
cat package.json.template | jq --arg pn "$INPUT_NAME" '.name="@birdiecare/\($pn)-client"' | jq --arg pn "$INPUT_NAME" '.description="Autogenerated API client for \($pn) service."' | jq --arg pn "$INPUT_NAME" '.repository.url="ssh://[email protected]/birdiecare/\($pn).git"' | jq --arg date "$DATE" '.version="0.0.\($date)"' > package.json
cat package.json.template \
| jq --arg NAME "$INPUT_PACKAGE_NAME" \
--arg REGISTRY "$INPUT_REGISTRY_NAMESPACE" \
'.name="\($REGISTRY)/\($NAME)"' \
| jq --arg NAME "$INPUT_PACKAGE_NAME" \
'.description="\($NAME): API client generated using openapi-generator and @birdiecare/hatch"' \
| jq --arg REPO_URL "$INPUT_REPOSITORY_URL" \
'.repository.url="\($REPO_URL)"' \
| jq --arg VERSION "$DATE" \
'.version="0.0.\($VERSION)"' \
> package.json

if [ $? -ne 0 ]
then
echo "ERROR: something went wrong writing package.json; aborting."
Expand All @@ -68,15 +87,15 @@ then
fi
echo "[SUCCESS]"

# Step 5: Fetch current version, if available, and diff against new
echo "Fetching latest published version of client package..."
# build npmrc for access to private package repo - also required to publish in next step
echo "@$INPUT_REGISTRY_NAMESPACE:registry=https://npm.pkg.github.com/" >> .npmrc
echo "//npm.pkg.github.com/:_authToken=$INPUT_TOKEN" >> .npmrc
echo "$INPUT_REGISTRY_NAMESPACE:registry=https://npm.pkg.github.com/" >> .npmrc
echo "//npm.pkg.github.com/:_authToken=$INPUT_REGISTRY_TOKEN" >> .npmrc
mkdir old_version
cp .npmrc old_version/
cd old_version
npm init --yes
npm i @$INPUT_REGISTRY_NAMESPACE/$INPUT_NAME-client
npm i $INPUT_REGISTRY_NAMESPACE/$INPUT_PACKAGE_NAME
if [ $? -eq 0 ]
then
echo "[SUCCESS]"
Expand All @@ -85,9 +104,9 @@ then
cksum dist/*.js | awk '{print $1":"$2}' >> new_checksums
cksum dist/*.d.ts | awk '{print $1":"$2}' >> new_checksums
cksum dist/models/** | awk '{print $1":"$2}' >> new_checksums
cksum old_version/node_modules/@$INPUT_REGISTRY_NAMESPACE/$INPUT_NAME-client/*.js | awk '{print $1":"$2}' >> old_checksums
cksum old_version/node_modules/@$INPUT_REGISTRY_NAMESPACE/$INPUT_NAME-client/*.d.ts | awk '{print $1":"$2}' >> old_checksums
cksum old_version/node_modules/@$INPUT_REGISTRY_NAMESPACE/$INPUT_NAME-client/models/** | awk '{print $1":"$2}' >> old_checksums
cksum old_version/node_modules/$INPUT_REGISTRY_NAMESPACE/$INPUT_PACKAGE_NAME/*.js | awk '{print $1":"$2}' >> old_checksums
cksum old_version/node_modules/$INPUT_REGISTRY_NAMESPACE/$INPUT_PACKAGE_NAME/*.d.ts | awk '{print $1":"$2}' >> old_checksums
cksum old_version/node_modules/$INPUT_REGISTRY_NAMESPACE/$INPUT_PACKAGE_NAME/models/** | awk '{print $1":"$2}' >> old_checksums
diff old_checksums new_checksums
if [ $? -eq 0 ]
then
Expand All @@ -100,12 +119,12 @@ then
fi
echo "Differences found between old and new package"
else
echo "ERROR: failed to fetch latest published version; skipping calculating differences"
echo "Failed to fetch latest published version; skipping calculating differences"
cd ..
fi

# Step 6: Publish package if there have been changes
echo "Publishing new package..."
# copy package.json to dist folder and publish to GitHub package registry
cp package.json dist/
cp .npmrc dist/
cd dist
Expand Down

0 comments on commit 878309a

Please sign in to comment.