Skip to content

Commit 409fca5

Browse files
committed
🔨 Add tooling for generating documentation
1 parent f33cc39 commit 409fca5

File tree

4 files changed

+333
-0
lines changed

4 files changed

+333
-0
lines changed

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module github.com/bitwizeshift/actions-github
2+
3+
go 1.21.1
4+
5+
require (
6+
github.com/iancoleman/strcase v0.3.0
7+
gopkg.in/yaml.v3 v3.0.1
8+
)

go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
2+
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
3+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
4+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
5+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# `actions-github`
2+
3+
This repository provides easy-to-use [composite actions] for managing a Github
4+
repository in [Github workflows]. All actions wrap the [octokit api] via
5+
[`actions/github-script`][github-script].
6+
7+
## Available Actions
8+
9+
Below are the list of available actions, with links to documentation:
10+
11+
{{.ActionTable}}
12+
## License
13+
14+
Except where otherwise specified, this project is dual-licensed under both the
15+
[Apache-2] and [MIT] licenses.
16+
17+
[Apache-2]: https://opensource.org/license/apache-2-0/
18+
[MIT]: http://opensource.org/licenses/MIT/
19+
[composite actions]: https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
20+
[octokit api]: https://octokit.github.io/rest.js/v20
21+
[Github workflows]: https://docs.github.com/en/actions/using-workflows
22+
[github-script]: https://github.com/actions/github-script

tools/generate-docs/main.go

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"html/template"
7+
"io"
8+
"io/fs"
9+
"os"
10+
"path"
11+
"runtime"
12+
"slices"
13+
"strings"
14+
15+
"github.com/iancoleman/strcase"
16+
yaml "gopkg.in/yaml.v3"
17+
)
18+
19+
// CompositeActionInput represents a Github composite action input field.
20+
type CompositeActionInput struct {
21+
Default string `yaml:"default"`
22+
Required bool `yaml:"required"`
23+
Description string `yaml:"description"`
24+
}
25+
26+
// CompositeActionOutput represents a Github composite action output field.
27+
type CompositeActionOutput struct {
28+
Description string `yaml:"description"`
29+
Value string `yaml:"value"`
30+
}
31+
32+
type CompositeAction struct {
33+
Name string `yaml:"name"`
34+
Description string `yaml:"description"`
35+
Inputs map[string]CompositeActionInput
36+
Outputs map[string]CompositeActionOutput
37+
Runs any `yaml:"runs"`
38+
}
39+
40+
func (ca *CompositeAction) RequiredInputNames() []string {
41+
var result []string
42+
for name, input := range ca.Inputs {
43+
if !input.Required {
44+
continue
45+
}
46+
result = append(result, name)
47+
}
48+
slices.Sort(result)
49+
return result
50+
}
51+
52+
func (ca *CompositeAction) OptionalInputNames() []string {
53+
var result []string
54+
for name, input := range ca.Inputs {
55+
if input.Required {
56+
continue
57+
}
58+
result = append(result, name)
59+
}
60+
slices.Sort(result)
61+
return result
62+
}
63+
64+
func (ca *CompositeAction) OutputNames() []string {
65+
var result []string
66+
for name := range ca.Outputs {
67+
result = append(result, name)
68+
}
69+
slices.Sort(result)
70+
return result
71+
}
72+
73+
func (ca *CompositeAction) WriteHeader(w io.Writer) {
74+
fmt.Fprintf(w, "# %v\n", ca.Name)
75+
fmt.Fprintf(w, "\n")
76+
fmt.Fprintf(w, "<!-- These docs are generated by a tool -->\n")
77+
fmt.Fprintf(w, "\n")
78+
fmt.Fprintf(w, "%v\n", ca.Description)
79+
}
80+
81+
func (ca *CompositeAction) WriteInputTable(w io.Writer) {
82+
fmt.Fprintf(w, "## Inputs\n")
83+
fmt.Fprintf(w, "\n")
84+
fmt.Fprintf(w, "| Name | Description | Default |\n")
85+
fmt.Fprintf(w, "|------|-------------|---------|\n")
86+
required := ca.RequiredInputNames()
87+
for _, name := range required {
88+
fields := ca.Inputs[name]
89+
value := "_N/A_"
90+
if !fields.Required {
91+
value = fmt.Sprintf("`\"%v\"`", fields.Default)
92+
}
93+
description := strings.ReplaceAll(fields.Description, "\n", " ")
94+
fmt.Fprintf(w, "| `%v` (*) | %v | %v |\n", name, description, value)
95+
}
96+
for _, name := range ca.OptionalInputNames() {
97+
fields := ca.Inputs[name]
98+
value := fmt.Sprintf("`\"%v\"`", fields.Default)
99+
description := strings.ReplaceAll(fields.Description, "\n", " ")
100+
fmt.Fprintf(w, "| `%v` | %v | %v |\n", name, description, value)
101+
}
102+
if len(required) > 0 {
103+
fmt.Fprintf(w, "\n")
104+
fmt.Fprintf(w, "**Note:** _(*) marks required inputs_\n")
105+
}
106+
107+
fmt.Fprintf(w, "\n")
108+
}
109+
110+
func (ca *CompositeAction) WriteOutputTable(w io.Writer, path string) {
111+
fmt.Fprintf(w, "## Outputs\n")
112+
fmt.Fprintf(w, "\n")
113+
if len(ca.Outputs) == 0 {
114+
fmt.Fprintf(w, "`%v` does not have any outputs at this time\n", path)
115+
fmt.Fprintf(w, "\n")
116+
} else {
117+
fmt.Fprintf(w, "| Name | Description |\n")
118+
fmt.Fprintf(w, "|------|-------------|\n")
119+
for _, name := range ca.OutputNames() {
120+
fields := ca.Outputs[name]
121+
description := strings.ReplaceAll(fields.Description, "\n", " ")
122+
fmt.Fprintf(w, "| `%v` | %v |\n", name, description)
123+
}
124+
fmt.Fprintf(w, "\n")
125+
}
126+
}
127+
128+
func (ca *CompositeAction) WriteExample(w io.Writer, path, version string) {
129+
id := strcase.ToKebab(strings.ReplaceAll(ca.Name, " ", "_"))
130+
fmt.Fprintf(w, "## Example\n")
131+
fmt.Fprintf(w, "\n")
132+
fmt.Fprintf(w, "Here is a very basic example of how to use the `%v` composite action\n", path)
133+
fmt.Fprintf(w, "in a project (placeholders are used in place of real inputs):\n")
134+
fmt.Fprintf(w, "\n")
135+
fmt.Fprintf(w, "```yaml\n")
136+
fmt.Fprintf(w, "run:\n")
137+
fmt.Fprintf(w, " example-job:\n")
138+
fmt.Fprintf(w, " # ... \n")
139+
fmt.Fprintf(w, " steps:\n")
140+
fmt.Fprintf(w, " # ... \n")
141+
fmt.Fprintf(w, " - name: %v\n", ca.Name)
142+
if len(ca.Outputs) > 0 {
143+
output := "output"
144+
if len(ca.Outputs) > 1 {
145+
output = "output(s)"
146+
}
147+
fmt.Fprintf(w, " id: %v # only necessary if using this action's %v\n", id, output)
148+
}
149+
fmt.Fprintf(w, " uses: bitwizeshift/actions-github/%v@%v\n", path, version)
150+
fmt.Fprintf(w, " with:\n")
151+
required := ca.RequiredInputNames()
152+
optional := ca.OptionalInputNames()
153+
154+
if len(required) > 0 {
155+
fmt.Fprintf(w, " # Required inputs\n")
156+
for _, name := range required {
157+
value := strcase.ToScreamingSnake(name)
158+
fmt.Fprintf(w, " %v: %v\n", name, value)
159+
}
160+
}
161+
if len(required) > 0 && len(optional) > 0 {
162+
fmt.Fprintf(w, "\n")
163+
}
164+
if len(optional) > 0 {
165+
fmt.Fprintf(w, " # Optional inputs\n")
166+
for _, name := range optional {
167+
value := strcase.ToScreamingSnake(name)
168+
fmt.Fprintf(w, " %v: %v\n", name, value)
169+
}
170+
}
171+
if len(ca.Outputs) > 0 {
172+
fmt.Fprintf(w, " # ... \n")
173+
fmt.Fprintf(w, " - name: Uses \"%v\" Outputs\n", ca.Name)
174+
fmt.Fprintf(w, " uses: example-actions/use-%v@v3 # illustrative\n", id)
175+
fmt.Fprintf(w, " with:\n")
176+
for _, name := range ca.OutputNames() {
177+
fmt.Fprintf(w, " use-%v: ${{ steps.%v.outputs.%v }}\n", name, id, name)
178+
}
179+
}
180+
fmt.Fprintf(w, "```\n")
181+
}
182+
183+
func (ca *CompositeAction) WriteMarkdown(w io.Writer, path, version string) {
184+
ca.WriteHeader(w)
185+
ca.WriteInputTable(w)
186+
ca.WriteOutputTable(w, path)
187+
ca.WriteExample(w, path, version)
188+
}
189+
190+
func findAllActionPaths(root string) ([]string, error) {
191+
result, err := findAllActionPathsAux(root)
192+
if err != nil {
193+
return nil, err
194+
}
195+
// Force all found paths to be relative to the root
196+
for i := range result {
197+
result[i], _ = strings.CutPrefix(result[i], root)
198+
}
199+
return result, nil
200+
}
201+
202+
func findAllActionPathsAux(root string) ([]string, error) {
203+
var result []string
204+
entries, err := os.ReadDir(root)
205+
if err != nil {
206+
return nil, err
207+
}
208+
209+
for _, entry := range entries {
210+
// Skip any hidden files
211+
if strings.HasPrefix(entry.Name(), ".") {
212+
continue
213+
}
214+
subpath := path.Join(root, entry.Name())
215+
if entry.Type() == fs.ModeDir {
216+
next, err := findAllActionPathsAux(subpath)
217+
if err != nil {
218+
return nil, err
219+
}
220+
result = append(result, next...)
221+
} else if entry.Name() == "action.yaml" {
222+
result = append(result, root)
223+
}
224+
}
225+
return result, nil
226+
}
227+
228+
var (
229+
inpath = flag.String("path", ".", "The input path to recursively search under")
230+
version = flag.String("version", "v1", "The version to use for generated examples")
231+
)
232+
233+
func sourceFileLocation() string {
234+
_, file, _, _ := runtime.Caller(0)
235+
return path.Dir(file)
236+
}
237+
238+
func main() {
239+
flag.Parse()
240+
241+
paths, err := findAllActionPaths(*inpath)
242+
if err != nil {
243+
panic(err)
244+
}
245+
246+
outfiles := map[string]string{}
247+
for _, p := range paths {
248+
filepath := path.Join(p, "action.yaml")
249+
file, err := os.Open(filepath)
250+
if err != nil {
251+
panic(err)
252+
}
253+
defer file.Close()
254+
var action CompositeAction
255+
if err := yaml.NewDecoder(file).Decode(&action); err != nil {
256+
panic(err)
257+
}
258+
outfile := fmt.Sprintf("%v.md", strings.ReplaceAll(p, "/", "-"))
259+
output := path.Join("docs", outfile)
260+
writer, err := os.Create(output)
261+
if err != nil {
262+
panic(err)
263+
}
264+
defer writer.Close()
265+
action.WriteMarkdown(writer, p, *version)
266+
outfiles[p] = output
267+
}
268+
var outkeys []string
269+
for file := range outfiles {
270+
outkeys = append(outkeys, file)
271+
}
272+
273+
slices.Sort(outkeys)
274+
table := strings.Builder{}
275+
for _, p := range outkeys {
276+
outfile := outfiles[p]
277+
table.WriteString(fmt.Sprintf("* [`%v`](%v)\n", p, outfile))
278+
}
279+
data := struct {
280+
ActionTable string
281+
}{
282+
ActionTable: table.String(),
283+
}
284+
285+
templateFile := path.Join(sourceFileLocation(), "README.md.template")
286+
tmpl, err := template.New("README.md.template").ParseFiles(templateFile)
287+
if err != nil {
288+
panic(err)
289+
}
290+
readme, err := os.Create("README.md")
291+
if err != nil {
292+
panic(err)
293+
}
294+
defer readme.Close()
295+
if err := tmpl.Execute(readme, data); err != nil {
296+
panic(err)
297+
}
298+
}

0 commit comments

Comments
 (0)