feat: Build framework for generating API docs (#5383)

* WIP

* Gen

* WIP

* chi swagger

* WIP

* WIP

* WIP

* GetWorkspaces

* GetWorkspaces

* Markdown

* Use widdershins

* WIP

* WIP

* WIP

* Markdown template

* Fix: makefile

* fmt

* Fix: comment

* Enable swagger conditionally

* fix: site

* Default false

* Flag tests

* fix

* fix

* template fixes

* Fix

* Fix

* Fix

* WIP

* Formatted

* Cleanup

* Templates

* BEGIN END SECTION

* subshell exit code

* Fix

* Fix merge

* WIP

* Fix

* Fix fmt

* Fix

* Generic api.md page

* Fix merge

* Link pages

* Fix

* Fix

* Fix: links

* Add icon

* Write manifest file

* Fix fmt

* Fix: enterprise

* Fix: Swagger.Enable

* Fix: rename apidocs to apidoc

* Fix: find -not -prune

* Fix: json not available

* Fix: rename Coderd API to Coder API

* Fix: npm exec

* Fix: api dir

* Fix: by ID

* Fix: string uuid

* Fix: include deleted

* Fix: indirect go.mod

* Fix: source lib.sh

* Fix: shellcheck

* Fix: pushd popd

* Fix: fmt

* Fix: improve workspaces

* Fix: swagger-enable

* Fix

* Fix: mention only HTTP 200

* Fix: IDs

* Fix: https

* Fix: icon

* More APis

* Fix: format swagger.json

* Fix: SwaggerEndpoint

* Fix: SCRIPT_DIR

* Fix: PROJECT_ROOT

* Fix: use code tags in schemas.md

* Fix: examples

* Fix: examples

* Fix: improve format

* Fix: date-time,enums

* Fix: include_deleted

* Fix: array of

* Fix: parameter, response

* Fix: string time or null

* Workspaces: more docs

* Workspaces: more docs

* Fix: renderDisplayName

* Fix: ActiveUserCount

* Fix

* Fix: typo

* Templates: docs

* Notice: incomplete
This commit is contained in:
Marcin Tojek
2022-12-19 18:43:46 +01:00
committed by GitHub
parent f239ca7ee3
commit dc6d271293
41 changed files with 9272 additions and 76 deletions
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# This script generates swagger description file and required Go docs files
# from the coderd API.
set -euo pipefail
# shellcheck source=scripts/lib.sh
source "$(dirname "$(dirname "${BASH_SOURCE[0]}")")/lib.sh"
APIDOCGEN_DIR=$(dirname "${BASH_SOURCE[0]}")
API_MD_TMP_FILE=$(mktemp /tmp/coder-apidocgen.XXXXXX)
cleanup() {
rm -f "${API_MD_TMP_FILE}"
}
trap cleanup EXIT
log "Use temporary file: ${API_MD_TMP_FILE}"
pushd "${PROJECT_ROOT}"
go run github.com/swaggo/swag/cmd/swag@v1.8.6 init \
--generalInfo="coderd.go" \
--dir="./coderd,./codersdk" \
--output="./coderd/apidoc" \
--outputTypes="go,json" \
--parseDependency=true
popd
pushd "${APIDOCGEN_DIR}"
npm ci
# Make sure that widdershins is installed correctly.
npm exec -- widdershins --version
# Render the Markdown file.
npm exec -- widdershins \
--user_templates "./markdown-template" \
--search false \
--omitHeader true \
--language_tabs "shell:curl" \
--summary "../../coderd/apidoc/swagger.json" \
--outfile "${API_MD_TMP_FILE}"
# Perform the postprocessing
go run postprocess/main.go -in-md-file-single "${API_MD_TMP_FILE}"
popd
@@ -0,0 +1,64 @@
## Swagger / OpenAPI 2 and OpenAPI 3 template parameters
Note that properties of OpenAPI objects will be in OpenAPI 3.0 form, as
Swagger / OpenAPI 2.0 definitions are converted automatically.
### Code templates
* `method` - the HTTP method of the operation (in lower-case)
* `methodUpper` - the HTTP method of the operation (in upper-case)
* `url` - the full URL of the operation (including protocol and host)
* `consumes[]` - an array of MIME-types the operation consumes
* `produces[]` - an array of MIME-types the operation produces
* `operation` - the current operation object
* `operationId` - the current operation id
* `opName` - the operationId if set, otherwise the method + path
* `tags[]` - the full list of tags applying to the operation
* `security` - the security definitions applying to the operation
* `resource` - the current tag/path object
* `parameters[]` - an array of parameters for the operation (see below)
* `queryString` - an example queryString, urlEncoded
* `requiredQueryString` - an example queryString for `required:true` parameters
* `queryParameters[]` - a subset of `parameters` that are `in:query`
* `requiredParameters[]` - a subset of `queryParameters` that are `required:true`
* `headerParameters[]` - a subset of `parameters` that are `in:header`
* `allHeaders[]` - a concatenation of `headerParameters` and pseudo-parameters `Accept` and `Content-Type`, and optionally `Authorization` (the latter has an `isAuth` boolean property set true so it can be omitted in templates if desired
### Parameter template
* `parameters[]` - an array of [parameters](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#parameterObject), including the following pseudo-properties
* `shortDesc` - a truncated version of the parameter description
* `safeType` - a computed version of the parameter type, including Body and schema names
* `originalType` - the original type of the parameter
* `exampleValues` - an object containing examples for use in code-templates
* `json` - example values in JSON compatible syntax
* `object` - example values in raw object form (unquoted strings etc)
* `depth` - a zero-based indicator of the depth of expanded request body parameters
* `enums[]` - an array of (parameter)name/value pairs
### Responses template
* `responses[]` - an array of [responses](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#responseObject), including `status` and `meaning` properties
### Authentication template
* `authenticationStr` - a simple string of methods (and scopes where appropriate)
* `securityDefinitions[]` - an array of applicable [securityDefinitions](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject)
### Schema Property template
* `schemaProperties[]` - an array of
* `name`
* `type`
* `required`
* `description`
* `enums[]` - an array of (schema property)name/value pairs
### Common to all templates
* `openapi` - the top-level OpenAPI / Swagger document
* `header` - the front-matter of the Slate/Shins markdown document
* `host` - the (computed) host of the API
* `protocol` - the default/first protocol of the API
* `baseUrl` - the (computed) baseUrl of the API (including protocol and host)
* `widdershins` - the contents of widdershins `package.json`
@@ -0,0 +1 @@
To perform this operation, you must be authenticated by means of one of the following methods: **{{= data.utils.getAuthenticationStr(data) }}**.
@@ -0,0 +1,4 @@
# Example request using curl
curl -X {{=data.methodUpper}} http://coder-server:8080{{=data.url}}{{=data.requiredQueryString}}{{?data.allHeaders.length}} \{{?}}
{{~data.allHeaders :p:index}} -H '{{=p.name}}: {{=p.exampleValues.object}}'{{?index < data.allHeaders.length-1}} \{{?}}
{{~}}
@@ -0,0 +1 @@
{{= data.utils.inspect(data) }}
@@ -0,0 +1,157 @@
{{
function renderSinglePropertyType(p) {
if (!p.$ref) {
return p.type;
}
const pRef = p.$ref.replace("#/components/schemas/","");
if (pRef == "codersdk.NullTime") {
return "string(time) or `null`";
}
return "[" + pRef + "](#" + pRef.replace(".","").toLowerCase() + ")";
}
function renderPropertyType(p) {
if (p.type == "array") {
return "array of " + renderSinglePropertyType(p.schema.items);
}
return renderSinglePropertyType(p);
}
function renderDisplayName(p) {
if (p.displayName == "» **additionalProperties**") {
return "» `[any property]`";
}
if (p.displayName == "**additionalProperties**") {
return "`[any property]`";
}
return "`" + p.displayName + "`";
}
function renderDescription(p) {
if (!p.description) {
return "none";
}
const toSnakeCase = str =>
str
.match(/[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+/g)
.map(x => x.toLowerCase())
.join('_');
const words = p.description.split(' ');
if (words.length == 0) {
return "none";
}
const countUppercase = words[0].length - words[0].replace(/[A-Z]/g, '').length;
if (countUppercase > 1) {
const displayName = p.displayName.charAt(0).toUpperCase() + p.displayName.replaceAll("_", " ").toLowerCase().slice(1);
return displayName + " " + words.slice(1).join(' ');
}
return p.description;
}
}}
{{? data.api.components && data.api.components.securitySchemes }}{{#def.security}}{{?}}
{{ for (var r in data.resources) { }}
{{ data.resource = data.resources[r]; }}
<!-- APIDOCGEN: BEGIN SECTION -->
{{= data.tags.section }}# {{= r}}
> This page is incomplete, stay tuned.
{{? data.resource.description }}{{= data.resource.description}}{{?}}
{{ for (var m in data.resource.methods) { }}
{{ data.operationUniqueName = m; }}
{{ data.method = data.resource.methods[m]; }}
{{ data.operationUniqueSlug = data.method.slug; }}
{{ data.operation = data.method.operation; }}
{{= data.templates.operation(data) }}
{{ } /* of methods */ }}
{{= data.tags.endSection }}
{{ } /* of resources */ }}
{{? data.api.components && data.api.components.schemas }}
{{= data.tags.section }}
<!-- APIDOCGEN: BEGIN SECTION -->
# Schemas
> This page is incomplete, stay tuned.
{{ for (var s in data.components.schemas) {
if (s == "codersdk.NullTime") {
continue;
}
}}
{{ var origSchema = data.components.schemas[s]; }}
{{ var schema = data.api.components.schemas[s]; }}
{{= data.tags.section }}
## {{=s}}
{{? data.options.yaml }}
```yaml
{{=data.utils.yaml.stringify(data.utils.getSample(schema,data.options,{quiet:true},data.api))}}
{{??}}
```json
{{=data.utils.safejson(data.utils.getSample(schema,data.options,{quiet:true},data.api),null,2)}}
{{?}}```
{{ var enums = []; }}
{{ var blocks = data.utils.schemaToArray(origSchema,-1,{trim:true,join:true},data); }}
{{ for (var block of blocks) {
for (var p of block.rows) {
if (p.schema && p.schema.enum) {
for (var e of p.schema.enum) {
enums.push({name:p.name,value:e});
}
}
}
}
}}
{{~ blocks :block}}
{{? block.title }}{{= block.title}}{{= '\n\n'}}{{?}}
{{? block.externalDocs}}
<a href="{{=block.externalDocs.url}}">{{=block.externalDocs.description||'External documentation'}}</a>
{{?}}
{{? block===blocks[0] }}
{{= data.tags.section }}
### Properties
{{?}}
{{? block.rows.length}}|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|{{?}}
{{~ block.rows :p}}|{{= renderDisplayName(p)}}|{{= renderPropertyType(p)}}|{{=p.required}}|{{=p.restrictions||'none'}}|{{= renderDescription(p)}}|
{{~}}
{{~}}
{{? (blocks[0].rows.length === 0) && (blocks.length === 1) }}
*None*
{{?}}
{{? enums.length > 0 }}
{{= data.tags.section }}
#### Enumerated Values
|Property|Value|
|---|---|
{{~ enums :e}}|{{=e.name}}|{{=data.utils.toPrimitive(e.value)}}|
{{~}}
{{= data.tags.endSection }}
{{?}}
{{= data.tags.endSection }}
{{= data.tags.endSection }}
{{ } /* of schemas */ }}
{{?}}
@@ -0,0 +1,48 @@
{{= data.tags.section }}
## {{= data.operationUniqueName}}
{{ data.methodUpper = data.method.verb.toUpperCase(); }}
{{ data.url = data.utils.slashes(data.baseUrl + data.method.path); }}
{{ data.parameters = data.operation.parameters; }}
{{ data.enums = []; }}
{{ data.utils.fakeProdCons(data); }}
{{ data.utils.fakeBodyParameter(data); }}
{{ data.utils.mergePathParameters(data); }}
{{ data.utils.getParameters(data); }}
{{? data.options.codeSamples || data.operation["x-code-samples"] }}
### Code samples
{{= data.utils.getCodeSamples(data)}}
{{?}}
`{{= data.methodUpper}} {{=data.method.path}}`
{{? data.operation.summary && !data.options.tocSummary}}*{{= data.operation.summary }}*{{?}}
{{? data.operation.description}}{{= data.operation.description }}{{?}}
{{? data.operation.requestBody}}
> Body parameter
{{? data.bodyParameter.exampleValues.description }}
> {{= data.bodyParameter.exampleValues.description }}
{{?}}
{{= data.utils.getBodyParameterExamples(data) }}
{{?}}
{{? data.parameters && data.parameters.length }}
{{#def.parameters}}
{{?}}
{{#def.responses}}
{{ data.security = data.operation.security ? data.operation.security : data.api.security; }}
{{? data.security && data.security.length }}
{{#def.authentication}}
{{??}}
{{#def.authentication_none}}
{{?}}
{{= data.tags.endSection }}
@@ -0,0 +1,50 @@
{{
function renderParameterType(p) {
if (p.schema['x-widdershins-oldRef']) {
const aType = p.schema['x-widdershins-oldRef'].replace("#/components/schemas/","");
const href = aType.replace(".","").toLowerCase();
return "[" + aType + "](schemas.md#" + href + ")";
}
return p.safeType;
}
}}
{{= data.tags.section }}
### Parameters
|Name|In|Type|Required|Description|
|---|---|---|---|---|
{{~ data.parameters :p}}|{{=p.name}}|{{=p.in}}|{{= renderParameterType(p)}}|{{=p.required}}|{{=p.shortDesc || 'none'}}|
{{~}}
{{? data.longDescs }}
#### Detailed descriptions
{{~ data.parameters :p}}{{? p.shortDesc !== p.description}}
**{{=p.name}}**: {{=p.description}}{{?}}
{{~}}
{{?}}
{{~ data.parameters :p}}
{{? p.schema && p.schema.enum }}
{{~ p.schema.enum :e}}
{{ var entry = {}; entry.name = p.name; entry.value = e; data.enums.push(entry); }}
{{~}}
{{?}}
{{? p.schema && p.schema.items && p.schema.items.enum }}
{{~ p.schema.items.enum :e}}
{{ var entry = {}; entry.name = p.name; entry.value = e; data.enums.push(entry); }}
{{~}}
{{?}}
{{~}}
{{? data.enums && data.enums.length }}
#### Enumerated Values
|Parameter|Value|
|---|---|
{{~ data.enums :e}}|{{=e.name}}|{{=data.utils.toPrimitive(e.value)}}|
{{~}}
{{?}}
{{= data.tags.endSection }}
@@ -0,0 +1,113 @@
{{
function renderSingleResponseType(r) {
var content;
for (var ct in r.content) {
content = r.content[ct];
break;
}
if (!content) {
return "no schema";
}
var ref = content.schema["x-widdershins-oldRef"];
if (!ref) {
ref = content.schema.items["x-widdershins-oldRef"];
}
const aType = ref.replace("#/components/schemas/","");
const href = aType.replace(".","").toLowerCase();
return "[" + aType + "](schemas.md#" + href + ")";
}
function renderResponseType(r) {
if (r.type == "array") {
return "array of " + renderSingleResponseType(r);
}
return renderSingleResponseType(r);
}
}}
{{ data.responses = data.utils.getResponses(data); }}
{{ data.responseSchemas = false; }}
{{~ data.responses :response }}
{{ if (response.content) data.responseSchemas = true; }}
{{~}}
{{? data.responseSchemas }}
### Example responses
{{= data.utils.getResponseExamples(data) }}
{{?}}
{{= data.tags.section }}
### Responses
|Status|Meaning|Description|Schema|
|---|---|---|---|
{{~ data.responses :r}}|{{=r.status}}|{{=r.meaning}}|{{=r.description || 'none'}}|{{= renderResponseType(r)}}|
{{~}}
{{ data.responseSchemas = false; }}
{{~ data.responses :response }}
{{ if (response.content && !response.$ref && !data.utils.isPrimitive(response.type)) data.responseSchemas = true; }}
{{~}}
{{? data.responseSchemas }}
<h3 id="{{=data.operationUniqueSlug}}-responseschema">Response Schema</h3>
{{~ data.responses :response}}
{{? response.content && !response.$ref && !data.utils.isPrimitive(response.type)}}
{{? Object.keys(response.content).length }}
{{ var responseKey = Object.keys(response.content)[0]; }}
{{ var responseSchema = response.content[responseKey].schema; }}
{{ var enums = []; }}
{{ var blocks = data.utils.schemaToArray(responseSchema,0,{trim:true,join:true},data); }}
{{ for (var block of blocks) {
for (var p of block.rows) {
if (p.schema && p.schema.enum) {
for (var e of p.schema.enum) {
enums.push({name:p.name,value:e});
}
}
}
}
}}
{{? blocks[0].rows.length || blocks[0].title }}
Status Code **{{=response.status}}**
{{~ blocks :block}}
{{? block.title }}*{{=block.title}}*
{{?}}
|Name|Type|Required|Restrictions|Description|
|---|---|---|---|---|
{{~block.rows :p}}|{{=p.displayName}}|{{? p.$ref}}`{{=p.$ref}}`{{?}}{{? !p.$ref}}{{=p.type}}{{?}}|{{=p.required}}|{{=p.restrictions||'none'}}|{{=p.description||'none'}}|
{{~}}
{{~}}
{{?}}
{{? enums.length > 0 }}
#### Enumerated Values
|Property|Value|
|---|---|
{{~ enums :e}}|{{=e.name}}|{{=data.utils.toPrimitive(e.value)}}|
{{~}}
{{?}}
{{?}}
{{ data.response = response; }}
{{?}}
{{~}}
{{?}}
{{ data.responseHeaders = data.utils.getResponseHeaders(data); }}
{{? data.responseHeaders.length }}
### Response Headers
|Status|Header|Type|Format|Description|
|---|---|---|---|---|
{{~ data.responseHeaders :h}}|{{=h.status}}|{{=h.header}}|{{=h.type}}|{{=h.format||''}}|{{=h.description||'none'}}|
{{~}}
{{?}}
{{= data.tags.endSection }}
@@ -0,0 +1,27 @@
<!-- APIDOCGEN: BEGIN SECTION -->
{{= data.tags.section }}# Authentication
{{ for (var s in data.api.components.securitySchemes) { }}
{{ var sd = data.api.components.securitySchemes[s]; }}
{{? sd.type == 'apiKey' }}
- API Key ({{=s}})
- Parameter Name: **{{=sd.name}}**, in: {{=sd.in}}. {{=sd.description || ''}}
{{?}}
{{? sd.type == 'http'}}
- HTTP Authentication, scheme: {{=sd.scheme}}{{? sd.description }}<br/>{{=sd.description}}{{?}}
{{?}}
{{? sd.type == 'oauth2'}}
- oAuth2 authentication. {{=sd.description || ''}}
{{ for (var f in sd.flows) { }}
{{ var flow = sd.flows[f]; }}
- Flow: {{=f}}
{{? flow.authorizationUrl}} - Authorization URL = [{{=flow.authorizationUrl}}]({{=flow.authorizationUrl}}){{?}}
{{? flow.tokenUrl}} - Token URL = [{{=flow.tokenUrl}}]({{=flow.tokenUrl}}){{?}}
{{? flow.scopes && Object.keys(flow.scopes).length}}
|Scope|Scope Description|
|---|---|
{{ for (var sc in flow.scopes) { }}|{{=sc}}|{{=data.utils.join(flow.scopes[sc])}}|
{{ } /* of scopes */ }}
{{?}}
{{ } /* of flows */ }}
{{?}}
{{ } /* of securitySchemes */ }}
+3369
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
{
"dependencies": {
"widdershins": "^4.0.1"
}
}
+206
View File
@@ -0,0 +1,206 @@
package main
import (
"bufio"
"bytes"
"encoding/json"
"flag"
"log"
"os"
"path"
"regexp"
"strings"
"golang.org/x/xerrors"
)
const (
apiSubdir = "api"
apiIndexFile = "index.md"
apiIndexContent = `Get started with Coder API:
<children>
This page is rendered on https://coder.com/docs/coder-oss/api. Refer to the other documents in the ` + "`" + `api/` + "`" + ` directory.
</children>
`
)
var (
docsDirectory string
inMdFileSingle string
sectionSeparator = []byte("<!-- APIDOCGEN: BEGIN SECTION -->\n")
nonAlphanumericRegex = regexp.MustCompile(`[^a-z0-9 ]+`)
)
func main() {
log.Println("Postprocess API docs")
flag.StringVar(&docsDirectory, "docs-directory", "../../docs", "Path to Coder docs directory")
flag.StringVar(&inMdFileSingle, "in-md-file-single", "", "Path to single Markdown file, output from widdershins.js")
flag.Parse()
if inMdFileSingle == "" {
flag.Usage()
log.Fatal("missing value for in-md-file-single")
}
sections, err := loadMarkdownSections()
if err != nil {
log.Fatal("can't load markdown sections: ", err)
}
err = prepareDocsDirectory()
if err != nil {
log.Fatal("can't prepare docs directory: ", err)
}
err = writeDocs(sections)
if err != nil {
log.Fatal("can't write docs directory: ", err)
}
log.Println("Done")
}
func loadMarkdownSections() ([][]byte, error) {
log.Printf("Read the md-file-single: %s", inMdFileSingle)
mdFile, err := os.ReadFile(inMdFileSingle)
if err != nil {
return nil, xerrors.Errorf("can't read the md-file-single: %w", err)
}
log.Printf("Read %dB", len(mdFile))
sections := bytes.Split(mdFile, sectionSeparator)
if len(sections) < 2 {
return nil, xerrors.Errorf("At least 1 section is expected: %w", err)
}
sections = sections[1:] // Skip the first element which is the empty byte array
log.Printf("Loaded %d sections", len(sections))
return sections, nil
}
func prepareDocsDirectory() error {
log.Println("Prepare docs directory")
apiPath := path.Join(docsDirectory, apiSubdir)
err := os.RemoveAll(apiPath)
if err != nil {
return xerrors.Errorf(`os.RemoveAll failed for "%s": %w`, apiPath, err)
}
err = os.MkdirAll(apiPath, 0755)
if err != nil {
return xerrors.Errorf(`os.MkdirAll failed for "%s": %w`, apiPath, err)
}
return nil
}
func writeDocs(sections [][]byte) error {
log.Println("Write docs to destination")
apiDir := path.Join(docsDirectory, apiSubdir)
err := os.WriteFile(path.Join(apiDir, apiIndexFile), []byte(apiIndexContent), 0644) // #nosec
if err != nil {
return xerrors.Errorf(`can't write the index file: %w`, err)
}
type mdFile struct {
title string
path string
}
var mdFiles []mdFile
// Write .md files for grouped API method (Templates, Workspaces, etc.)
for _, section := range sections {
sectionName, err := extractSectionName(section)
if err != nil {
return xerrors.Errorf("can't extract section name: %w", err)
}
log.Printf("Write section: %s", sectionName)
mdFilename := toMdFilename(sectionName)
docPath := path.Join(apiDir, mdFilename)
err = os.WriteFile(docPath, section, 0644) // #nosec
if err != nil {
return xerrors.Errorf(`can't write doc file "%s": %w`, docPath, err)
}
mdFiles = append(mdFiles, mdFile{
title: sectionName,
path: "./" + path.Join(apiSubdir, mdFilename),
})
}
// Update manifest.json
type route struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Path string `json:"path,omitempty"`
IconPath string `json:"icon_path,omitempty"`
State string `json:"state,omitempty"`
Children []route `json:"children,omitempty"`
}
type manifest struct {
Versions []string `json:"versions,omitempty"`
Routes []route `json:"routes,omitempty"`
}
manifestPath := path.Join(docsDirectory, "manifest.json")
manifestFile, err := os.ReadFile(manifestPath)
if err != nil {
return xerrors.Errorf("can't read manifest file: %w", err)
}
log.Printf("Read manifest file: %dB", len(manifestFile))
var m manifest
err = json.Unmarshal(manifestFile, &m)
if err != nil {
return xerrors.Errorf("json.Unmarshal failed: %w", err)
}
for i, r := range m.Routes {
if r.Title != "API" {
continue
}
var children []route
for _, mdf := range mdFiles {
docRoute := route{
Title: mdf.title,
Path: mdf.path,
}
children = append(children, docRoute)
}
m.Routes[i].Children = children
break
}
manifestFile, err = json.MarshalIndent(m, "", " ")
if err != nil {
return xerrors.Errorf("json.Marshal failed: %w", err)
}
err = os.WriteFile(manifestPath, manifestFile, 0644) // #nosec
if err != nil {
return xerrors.Errorf("can't write manifest file: %w", err)
}
log.Printf("Write manifest file: %dB", len(manifestFile))
return nil
}
func extractSectionName(section []byte) (string, error) {
scanner := bufio.NewScanner(bytes.NewReader(section))
if !scanner.Scan() {
return "", xerrors.Errorf("section header was expected")
}
header := scanner.Text()[2:] // Skip #<space>
return strings.TrimSpace(header), nil
}
func toMdFilename(sectionName string) string {
return nonAlphanumericRegex.ReplaceAllLiteralString(strings.ToLower(sectionName), "-") + ".md"
}
+1 -1
View File
@@ -121,7 +121,7 @@ fatal() {
trap 'fatal "Script encountered an error"' ERR
cdroot
start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000
start_cmd API "" "${CODER_DEV_SHIM}" server --http-address 0.0.0.0:3000 --swagger-enable
echo '== Waiting for Coder to become ready'
# Start the timeout in the background so interrupting this script
+1 -1
View File
@@ -26,7 +26,7 @@ var (
func main() {
flag.StringVar(&metricsFile, "metrics-file", "scripts/metricsdocgen/metrics", "Path to Prometheus metrics file")
flag.StringVar(&prometheusDocFile, "prometheus-doc-file", "docs/admin/prometheus.md", "Path to prometheus doc file")
flag.StringVar(&prometheusDocFile, "prometheus-doc-file", "docs/admin/prometheus.md", "Path to Prometheus doc file")
flag.BoolVar(&dryRun, "dry-run", false, "Dry run")
flag.Parse()