Skip to content

Commit dbe8350

Browse files
petarlidel
andauthored
feat: automate CLI and HTTP API doc generation (#875)
* move http-api-docs here * add automation to regenerate api on go-ipfs tag * add cli docs generation * refactor: pin third-party action * style: update-on-new-ipfs-tag * chore: schedule/cron Co-authored-by: Marcin Rataj <[email protected]>
1 parent 4bb097d commit dbe8350

File tree

19 files changed

+2395
-0
lines changed

19 files changed

+2395
-0
lines changed

Diff for: .github/actions/latest-ipfs-tag/Dockerfile

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM golang:1.17
2+
COPY entrypoint.sh /entrypoint.sh
3+
ENTRYPOINT ["/entrypoint.sh"]

Diff for: .github/actions/latest-ipfs-tag/action.yml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
name: 'Find latest go-ipfs tag'
2+
outputs:
3+
latest_tag:
4+
description: "latest go-ipfs tag name"
5+
runs:
6+
using: 'docker'
7+
image: 'Dockerfile'

Diff for: .github/actions/latest-ipfs-tag/entrypoint.sh

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#!/usr/bin/env sh
2+
set -eu
3+
4+
# extract IPFS release
5+
cd /tmp
6+
git clone https://github.com/ipfs/go-ipfs.git
7+
cd go-ipfs
8+
LATEST_IPFS_TAG=`git describe --tags --abbrev=0`
9+
echo "The latest IPFS tag is ${LATEST_IPFS_TAG}"
10+
echo "::set-output name=latest_tag::${LATEST_IPFS_TAG}"

Diff for: .github/actions/update-on-new-ipfs-tag/Dockerfile

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
FROM golang:1.17
2+
COPY entrypoint.sh /entrypoint.sh
3+
ENTRYPOINT ["/entrypoint.sh"]

Diff for: .github/actions/update-on-new-ipfs-tag/action.yml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: 'Update on new go-ipfs tag'
2+
inputs:
3+
latest_ipfs_tag:
4+
description: "latest go ipfs tag"
5+
required: true
6+
outputs:
7+
updated_branch:
8+
description: "name of pushed branch with updated doc"
9+
runs:
10+
using: 'docker'
11+
image: 'Dockerfile'

Diff for: .github/actions/update-on-new-ipfs-tag/entrypoint.sh

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env sh
2+
set -eu
3+
4+
API_FILE=`pwd`/docs/reference/http/api.md
5+
ROOT=`pwd`
6+
cd tools/http-api-docs
7+
8+
# extract go-ipfs release tag used in http-api-docs from go.mod in this repo
9+
CURRENT_IPFS_TAG=`grep 'github.com/ipfs/go-ipfs ' ./go.mod | awk '{print $2}'`
10+
echo "The currently used go-ipfs tag in http-api-docs is ${CURRENT_IPFS_TAG}"
11+
12+
# extract IPFS release
13+
LATEST_IPFS_TAG=$INPUT_LATEST_IPFS_TAG
14+
echo "The latest IPFS tag is ${LATEST_IPFS_TAG}"
15+
16+
# make the upgrade, if newer go-ipfs tags exist
17+
if [ "$CURRENT_IPFS_TAG" = "$LATEST_IPFS_TAG" ]; then
18+
echo "http-api-docs already uses the latest go-ipfs tag."
19+
else
20+
# update http-api-docs
21+
git checkout -b bump-http-api-docs-ipfs-to-$LATEST_IPFS_TAG
22+
sed "s/^\s*github.com\/ipfs\/go-ipfs\s\+$CURRENT_IPFS_TAG\s*$/ github.com\/ipfs\/go-ipfs $LATEST_IPFS_TAG/" go.mod > go.mod2
23+
mv go.mod2 go.mod
24+
go mod tidy
25+
make
26+
http-api-docs > $API_FILE
27+
28+
# update cli docs
29+
cd $ROOT # go back to root of ipfs-docs repo
30+
git clone https://github.com/ipfs/go-ipfs.git
31+
cd go-ipfs
32+
git fetch --all --tags
33+
git checkout tags/$LATEST_IPFS_TAG
34+
go install ./cmd/ipfs
35+
cd $ROOT/docs/reference
36+
./generate-cli-docs.sh
37+
38+
# submit a PR
39+
cd $ROOT # go back to root of ipfs-docs repo
40+
git config --global user.email "${GITHUB_ACTOR}"
41+
git config --global user.name "${GITHUB_ACTOR}@users.noreply.github.com"
42+
git add -u
43+
git commit -m "Bumped go-ipfs dependence of http-api-docs to tag $LATEST_IPFS_TAG."
44+
git push -u origin bump-http-api-docs-ipfs-to-$LATEST_IPFS_TAG
45+
fi
46+
echo "::set-output name=updated_branch::bump-http-api-docs-ipfs-to-$LATEST_IPFS_TAG"

Diff for: .github/workflows/update-on-new-ipfs-tag.yml

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Update docs on new ipfs-tag release
2+
on:
3+
workflow_dispatch:
4+
schedule:
5+
- cron: '30 5,17 * * *' # run every day at 5:30am and 5:30pm UTC
6+
7+
jobs:
8+
update:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout ipfs-docs
12+
uses: actions/checkout@v2
13+
- name: Find latest go-ipfs tag
14+
id: latest_ipfs
15+
uses: ./.github/actions/latest-ipfs-tag
16+
- name: Update http-api-docs
17+
id: update
18+
uses: ./.github/actions/update-on-new-ipfs-tag
19+
with:
20+
latest_ipfs_tag: ${{ steps.latest_ipfs.outputs.latest_tag }}
21+
- name: pull-request # don't create a pr if there was no new ipfs tag
22+
uses: repo-sync/pull-request@65194d8015be7624d231796ddee1cd52a5023cb3 #v2.16
23+
with:
24+
github_token: ${{ secrets.GITHUB_TOKEN }}
25+
source_branch: ${{ steps.update.outputs.updated_branch }}
26+
destination_branch: "main"
27+
pr_title: "Bump CLI and HTTP API reference to go-ipfs ${{ steps.latest_ipfs.outputs.latest_tag }}"
28+
pr_body: "Release Notes: https://github.com/ipfs/go-ipfs/releases/${{ steps.latest_ipfs.outputs.latest_tag }}"
29+
pr_label: "needs/triage,P0"

Diff for: tools/http-api-docs/LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2016 Hector Sanjuan
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Diff for: tools/http-api-docs/Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
all: install
2+
install:
3+
GO111MODULE=on go install ./http-api-docs

Diff for: tools/http-api-docs/README.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# http-api-docs
2+
3+
[![](https://img.shields.io/badge/made%20by-Protocol%20Labs-blue.svg?style=flat-square)](http://ipn.io)
4+
[![](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](http://ipfs.io/)
5+
[![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme)
6+
[![Build Status](https://travis-ci.com/ipfs/http-api-docs.svg?branch=master)](https://travis-ci.org/ipfs/http-api-docs)
7+
8+
> A generator for go-ipfs API endpoints documentation.
9+
10+
Note: This is just the generator for the docs that are available on ipfs.io, which can be found here: https://docs.ipfs.io/reference/http/api/
11+
12+
The original docs are written in Markdown format and are available for community contributions here: https://github.com/ipfs/ipfs-docs
13+
14+
## Table of Contents
15+
16+
- [Install](#install)
17+
- [Usage](#usage)
18+
- [Captain](#captain)
19+
- [Contribute](#contribute)
20+
- [License](#license)
21+
22+
## Install
23+
24+
In order to build this project, you need to first install Go, clone this repo, and finally run `make install`:
25+
26+
```sh
27+
> git clone https://github.com/ipfs/http-api-docs "$(go env GOPATH)/src/github.com/ipfs/http-api-docs"
28+
> cd "$(go env GOPATH)/src/github.com/ipfs/http-api-docs"
29+
> make install
30+
```
31+
32+
## Usage
33+
34+
After installing you can run:
35+
36+
```
37+
> http-api-docs
38+
```
39+
40+
This should spit out a Markdown document. This is exactly the `api.md` documentation at https://github.com/ipfs/ipfs-docs/blob/master/docs/reference/http/api.md, so you can redirect the output to just overwrite that file.
41+
42+
## Captain
43+
44+
This project is captained by @hsanjuan.
45+
46+
## Contribute
47+
48+
PRs accepted.
49+
50+
Small note: If editing the README, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification.
51+
52+
## License
53+
54+
MIT (C) Protocol Labs, Inc.

Diff for: tools/http-api-docs/endpoints.go

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Package docs can be used to gather go-ipfs commands and automatically
2+
// generate documentation or tests.
3+
package docs
4+
5+
import (
6+
"fmt"
7+
"sort"
8+
9+
jsondoc "github.com/Stebalien/go-json-doc"
10+
cid "github.com/ipfs/go-cid"
11+
config "github.com/ipfs/go-ipfs"
12+
cmds "github.com/ipfs/go-ipfs-cmds"
13+
corecmds "github.com/ipfs/go-ipfs/core/commands"
14+
peer "github.com/libp2p/go-libp2p-core/peer"
15+
multiaddr "github.com/multiformats/go-multiaddr"
16+
)
17+
18+
var JsondocGlossary = jsondoc.NewGlossary().
19+
WithSchema(new(cid.Cid), jsondoc.Object{"/": "<cid-string>"}).
20+
WithName(new(multiaddr.Multiaddr), "multiaddr-string").
21+
WithName(new(peer.ID), "peer-id").
22+
WithSchema(new(peer.AddrInfo),
23+
jsondoc.Object{"ID": "peer-id", "Addrs": []string{"<multiaddr-string>"}})
24+
25+
var ignoreOptsPerEndpoint = map[string]map[string]struct{}{
26+
"/api/v0/add": {
27+
cmds.RecLong: struct{}{},
28+
cmds.DerefLong: struct{}{},
29+
cmds.StdinName: struct{}{},
30+
cmds.Hidden: struct{}{},
31+
cmds.Ignore: struct{}{},
32+
cmds.IgnoreRules: struct{}{},
33+
},
34+
}
35+
36+
// A map of single endpoints to be skipped (subcommands are processed though).
37+
var IgnoreEndpoints = map[string]bool{}
38+
39+
// How much to indent when generating the response schemas
40+
const IndentLevel = 4
41+
42+
// Failsafe when traversing objects containing objects of the same type
43+
const MaxIndent = 20
44+
45+
// Endpoint defines an IPFS RPC API endpoint.
46+
type Endpoint struct {
47+
Name string
48+
Arguments []*Argument
49+
Options []*Argument
50+
Description string
51+
Response string
52+
Group string
53+
}
54+
55+
// Argument defines an IPFS RPC API endpoint argument.
56+
type Argument struct {
57+
Endpoint string
58+
Name string
59+
Description string
60+
Type string
61+
Required bool
62+
Default string
63+
}
64+
65+
type sorter []*Endpoint
66+
67+
func (a sorter) Len() int { return len(a) }
68+
func (a sorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
69+
func (a sorter) Less(i, j int) bool { return a[i].Name < a[j].Name }
70+
71+
const APIPrefix = "/api/v0"
72+
73+
// AllEndpoints gathers all the endpoints from go-ipfs.
74+
func AllEndpoints() []*Endpoint {
75+
return Endpoints(APIPrefix, corecmds.Root)
76+
}
77+
78+
func IPFSVersion() string {
79+
return config.CurrentVersionNumber
80+
}
81+
82+
// Endpoints receives a name and a go-ipfs command and returns the endpoints it
83+
// defines] (sorted). It does this by recursively gathering endpoints defined by
84+
// subcommands. Thus, calling it with the core command Root generates all
85+
// the endpoints.
86+
func Endpoints(name string, cmd *cmds.Command) (endpoints []*Endpoint) {
87+
var arguments []*Argument
88+
var options []*Argument
89+
90+
ignore := cmd.Run == nil || IgnoreEndpoints[name]
91+
if !ignore { // Extract arguments, options...
92+
for _, arg := range cmd.Arguments {
93+
argType := "string"
94+
if arg.Type == cmds.ArgFile {
95+
argType = "file"
96+
}
97+
arguments = append(arguments, &Argument{
98+
Endpoint: name,
99+
Name: arg.Name,
100+
Type: argType,
101+
Required: arg.Required,
102+
Description: arg.Description,
103+
})
104+
}
105+
106+
for _, opt := range cmd.Options {
107+
if ignoreOpts, ok := ignoreOptsPerEndpoint[name]; ok {
108+
if _, ok := ignoreOpts[opt.Names()[0]]; ok {
109+
// skip this option for this endpoint.
110+
continue
111+
}
112+
}
113+
114+
def := fmt.Sprint(opt.Default())
115+
if def == "<nil>" {
116+
def = ""
117+
}
118+
options = append(options, &Argument{
119+
Name: opt.Names()[0],
120+
Type: opt.Type().String(),
121+
Description: opt.Description(),
122+
Default: def,
123+
})
124+
}
125+
126+
res := buildResponse(cmd.Type)
127+
128+
endpoints = []*Endpoint{
129+
{
130+
Name: name,
131+
Description: cmd.Helptext.Tagline,
132+
Arguments: arguments,
133+
Options: options,
134+
Response: res,
135+
},
136+
}
137+
}
138+
139+
for n, cmd := range cmd.Subcommands {
140+
endpoints = append(endpoints,
141+
Endpoints(fmt.Sprintf("%s/%s", name, n), cmd)...)
142+
}
143+
sort.Sort(sorter(endpoints))
144+
return endpoints
145+
}
146+
147+
func buildResponse(res interface{}) string {
148+
// Commands with a nil type return text. This is a bad thing.
149+
if res == nil {
150+
return "This endpoint returns a `text/plain` response body."
151+
}
152+
desc, err := JsondocGlossary.Describe(res)
153+
if err != nil {
154+
panic(err)
155+
}
156+
return desc
157+
}

Diff for: tools/http-api-docs/endpoints_test.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package docs
2+
3+
import "testing"
4+
5+
func TestEndpoints(t *testing.T) {
6+
AllEndpoints()
7+
}

Diff for: tools/http-api-docs/formatter.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package docs
2+
3+
import "bytes"
4+
5+
// Formatter allows to implement generation of docs in different formats.
6+
type Formatter interface {
7+
GenerateIntro() string
8+
GenerateIndex(endp []*Endpoint) string
9+
GenerateEndpointBlock(endp *Endpoint) string
10+
GenerateArgumentsBlock(args []*Argument, opts []*Argument) string
11+
GenerateBodyBlock(args []*Argument) string
12+
GenerateResponseBlock(response string) string
13+
GenerateExampleBlock(endp *Endpoint) string
14+
}
15+
16+
// GenerateDocs uses a formatter to generate documentation for every endpoint
17+
func GenerateDocs(api []*Endpoint, formatter Formatter) string {
18+
buf := new(bytes.Buffer)
19+
buf.WriteString(formatter.GenerateIntro())
20+
// In docs.ipfs.io this is handled by the TOC.
21+
// buf.WriteString(formatter.GenerateIndex(api))
22+
for _, endp := range api {
23+
buf.WriteString(formatter.GenerateEndpointBlock(endp))
24+
buf.WriteString(formatter.GenerateArgumentsBlock(endp.Arguments, endp.Options))
25+
buf.WriteString(formatter.GenerateBodyBlock(endp.Arguments))
26+
buf.WriteString(formatter.GenerateResponseBlock(endp.Response))
27+
buf.WriteString(formatter.GenerateExampleBlock(endp))
28+
}
29+
return buf.String()
30+
}

0 commit comments

Comments
 (0)