Skip to content

Commit

Permalink
Add github action to sync api docs to the user docs site
Browse files Browse the repository at this point in the history
  • Loading branch information
tinygrasshopper committed May 16, 2024
1 parent cc2499b commit 16dca25
Show file tree
Hide file tree
Showing 11 changed files with 363 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
# https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners

* @snyk/docs
tools/api-docs-generator/* @snyk/api
38 changes: 38 additions & 0 deletions .github/workflows/sync-api-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Synchronize API Docs

on:
workflow_dispatch:
schedule:
- cron: '0 * * * 1-5' # Mon-Fri every hour
push:
branches: [chore/docs-action]

jobs:
build:
name: synchronize-api-docs
runs-on: ubuntu-latest
steps:
- run: |
gh auth setup-git
git config --global user.email "[email protected]"
git config --global user.name "$GITHUB_ACTOR"
gh repo clone snyk/user-docs user-docs -- --depth=1 --quiet --branch chore/docs-action
cd ./user-docs
OUTPUT=$(cd tools/api-docs-generator && go mod tidy && go run . config.yml ../../)
if [[ $(git status --porcelain) ]]; then
echo "Documentation changes detected"
git --no-pager diff --name-only
git add .
git commit -m "docs: synchronizing api spec with user-docs"
git checkout -b docs/automatic-api-docs-update
git push --force origin docs/automatic-api-docs-update
if [[ ! $(gh pr view docs/automatic-api-docs-update 2>&1 | grep -q "no open pull requests";) ]]; then
echo "Creating PR"
gh pr create --title="Generate API docs from spec" --body="Automatic PR controlled by GitHub Action\n$OUTPUT" --head docs/automatic-api-docs-update
fi
echo "PR exists, pushed changes to it."
else
echo "No documentation changes detected, exiting."
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5 changes: 5 additions & 0 deletions tools/api-docs-generator/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
test:
go test ./...

run:
go run . config.yml ../..
30 changes: 30 additions & 0 deletions tools/api-docs-generator/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"gopkg.in/yaml.v3"
"os"
)

type config struct {
Fetcher struct {
Source string `yaml:"source"`
Destination string `yaml:"destination"`
} `yaml:"fetcher"`
Specs []struct {
Path string `yaml:"path"`
Suffix string `yaml:"suffix,omitempty"`
DocsHint string `yaml:"docsHint,omitempty"`
} `yaml:"specs"`
Output struct {
APIReferencePath string `yaml:"apiReferencePath"`
} `yaml:"output"`
}

func parseConfigFile(filename string) (config, error) {
cfg := config{}
file, err := os.Open(filename)
if err != nil {
return cfg, err
}
return cfg, yaml.NewDecoder(file).Decode(&cfg)
}
12 changes: 12 additions & 0 deletions tools/api-docs-generator/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
fetcher:
source: https://api.snyk.io/rest/openapi
destination: .gitbook/assets/rest-spec.json
specs:
- path: .gitbook/assets/spec.yaml
suffix: " (v1)"
docsHint: This document uses the v1 API. For more details, see the [v1 API](../v1-api).
- path: .gitbook/assets/rest-spec.json
docsHint: This document uses the REST API. For more details, see the [Authentication for API](../authentication-for-api/) page.

output:
apiReferencePath: snyk-api/reference
18 changes: 18 additions & 0 deletions tools/api-docs-generator/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module github.com/snyk/user-docs/tools/api-docs-generator

go 1.22.1

require (
github.com/getkin/kin-openapi v0.124.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/swag v0.22.8 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
)
38 changes: 38 additions & 0 deletions tools/api-docs-generator/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw=
github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY=
github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
25 changes: 25 additions & 0 deletions tools/api-docs-generator/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"log"
"os"
)

func main() {
if len(os.Args) != 3 {
log.Fatal("usage: api-docs <config-file> <docs-dir>")
}
cfg, err := parseConfigFile(os.Args[1])
if err != nil {
log.Fatal(err)
}
docsDirectory := os.Args[2]

err = fetchSpec(cfg, docsDirectory)

err = generateReferenceDocs(cfg, docsDirectory)
if err != nil {
log.Fatal(err)
}

}
101 changes: 101 additions & 0 deletions tools/api-docs-generator/reference_docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package main

import (
"fmt"
"github.com/getkin/kin-openapi/openapi3"
"os"
"path"
"sort"
"strings"
)

type operationPath struct {
operation *openapi3.Operation
pathItem *openapi3.PathItem
pathUrl string
specPath string
method string
docsHint string
}

func generateReferenceDocs(config config, docsPath string) error {
aggregatedDocs := map[string][]operationPath{}

assetPathBase := path.Join(docsPath, "docs")

for _, spec := range config.Specs {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromFile(path.Join(assetPathBase, spec.Path))
if err != nil {
return err
}
for pathUrl, pathItem := range doc.Paths.Map() {
for method, operation := range pathItem.Operations() {
for _, tag := range operation.Tags {
tag = tag + spec.Suffix
aggregatedDocs[tag] = append(aggregatedDocs[tag], operationPath{
operation: operation,
pathItem: pathItem,
pathUrl: pathUrl,
specPath: spec.Path,
method: method,
docsHint: spec.DocsHint,
})
}
}
}
}

var summary []string
for label, operation := range aggregatedDocs {
if label == "OpenAPI" {
continue
}
filePath := path.Join(docsPath, "docs/", config.Output.APIReferencePath, labelToFileName(label))
docsFile, err := os.Create(filePath)
if err != nil {
return err
}
summary = append(summary, fmt.Sprintf("* [%s](%s)\n", label, path.Join(config.Output.APIReferencePath, labelToFileName(label))))

fmt.Fprintf(docsFile, `# %s
{%% hint style="info" %%}
%s
{%% endhint %%}`, label, operation[0].docsHint)

// sort for stability
sort.Slice(operation, func(i, j int) bool {
return operation[i].pathUrl+operation[i].method > operation[j].pathUrl+operation[j].method
})

for _, op := range operation {
_, err = fmt.Fprintf(docsFile,
`
{%% swagger src="../../%s" path="%s" method="%s" %%}
[spec.yaml](../../%s)
{%% endswagger %%}
`,
op.specPath,
op.pathUrl,
strings.ToLower(op.method),
op.specPath,
)
if err != nil {
return err
}
}
}
sort.Strings(summary)
fmt.Printf("%s", strings.Join(summary, ""))

return nil
}

func labelToFileName(label string) string {
replacements := []string{"(", ")"}
for _, replacement := range replacements {
label = strings.ReplaceAll(label, replacement, "")
}
return strings.ToLower(strings.ReplaceAll(label, " ", "-")) + ".md"
}
66 changes: 66 additions & 0 deletions tools/api-docs-generator/spec_fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package main

import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
"os"
"path"
"sort"
"strings"
)

func fetchSpec(cfg config, directory string) error {
resp, err := http.Get(cfg.Fetcher.Source)
if err != nil {
return err
}

var versions []string

if err = json.NewDecoder(resp.Body).Decode(&versions); err != nil {
return err
}
if err = resp.Body.Close(); err != nil {
return err
}

gaVersion := getLatestGAVersion(versions)

specPath, err := url.JoinPath(cfg.Fetcher.Source, gaVersion)
if err != nil {
return err
}

resp, err = http.Get(specPath)
if err != nil {
return err
}
defer resp.Body.Close()

jsonSpec, err := io.ReadAll(resp.Body)
if err != nil {
return err
}

formattedSpec := bytes.NewBufferString("")
err = json.Indent(formattedSpec, jsonSpec, "", " ")
if err != nil {
return err
}

return os.WriteFile(path.Join(directory, "docs", cfg.Fetcher.Destination), formattedSpec.Bytes(), 0644)
}

func getLatestGAVersion(versions []string) string {
gaVersions := []string{}
for _, version := range versions {
if !strings.Contains(version, "~") {
gaVersions = append(gaVersions, version)
}
}
sort.Strings(gaVersions)
return gaVersions[len(gaVersions)-1]
}
29 changes: 29 additions & 0 deletions tools/api-docs-generator/spec_fetcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import "testing"

func Test_getLatestGAVersion(t *testing.T) {
tests := []struct {
name string
versions []string
want string
}{
{
name: "gets the latest version",
versions: []string{"2024-03-12~experimental", "2024-03-12~beta", "2024-03-12", "2024-03-15~experimental", "2024-03-15~beta", "2024-03-15", "2024-04-11~experimental", "2024-04-11~beta", "2024-04-11", "2024-04-22~experimental", "2024-04-22~beta", "2024-04-22", "2024-04-25~experimental", "2024-04-25~beta", "2024-04-25", "2024-04-29~experimental", "2024-04-29~beta", "2024-04-29", "2024-05-08~experimental", "2024-05-08~beta", "2024-05-08"},
want: "2024-05-08",
},
{
name: "gets the latest GA version",
versions: []string{"2024-03-12~experimental", "2024-03-12~beta", "2024-03-12", "2024-03-15~experimental", "2024-03-15~beta", "2024-03-15", "2024-04-11~experimental", "2024-04-11~beta", "2024-04-11", "2024-04-22~experimental", "2024-04-22~beta", "2024-04-22", "2024-04-25~experimental", "2024-04-25~beta", "2024-04-25", "2024-04-29~experimental", "2024-04-29~beta", "2024-04-29", "2024-05-08~experimental", "2024-05-08~beta"},
want: "2024-04-29",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getLatestGAVersion(tt.versions); got != tt.want {
t.Errorf("getLatestGAVersion() = %v, want %v", got, tt.want)
}
})
}
}

0 comments on commit 16dca25

Please sign in to comment.