Skip to content

Commit 28e1b28

Browse files
committed
add tool to automatically update docs/checks.md
1 parent 174d079 commit 28e1b28

File tree

3 files changed

+249
-13
lines changed

3 files changed

+249
-13
lines changed

docs/checks.md

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ jobs:
581581
```
582582

583583
Output:
584+
<!-- Skip update output -->
584585

585586
```
586587
test.yaml:8:23: property "my_action" is not defined in object type {} [expression]
@@ -593,6 +594,8 @@ test.yaml:15:23: property "some-value" is not defined in object type {some_value
593594
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
594595
```
595596
597+
<!-- Skip playground link -->
598+
596599
The 'My action with output' action defines one output `some_value`. The property is typed at `steps.my_action.outputs` object
597600
so that actionlint can check incorrect property accesses like a typo in the output name.
598601
@@ -756,6 +759,8 @@ actionlint defines a type of `needs` variable contextually by looking at each jo
756759
<a name="check-comparison-types"></a>
757760
## Strict type checks for comparison operators
758761

762+
Example input:
763+
759764
```yaml
760765
on:
761766
workflow_call:
@@ -788,6 +793,8 @@ test.yaml:16:17: "bool" value cannot be compared to "number" value with ">" oper
788793
| ^~~~~~~~~~~~~~
789794
```
790795
796+
[Playground](https://rhysd.github.io/actionlint#MISSING)
797+
791798
Expressions in `${{ }}` placeholders support `==`, `!=`, `>`, `>=`, `<`, `<=` comparison operators. Arbitrary types of operands
792799
can be compared. When different type values are compared, they are implicitly converted to numbers before the comparison. Please
793800
see [the official document][operators-doc] to know the details of operators behavior.
@@ -841,6 +848,8 @@ test.yaml:14:9: shellcheck reported issue in this script: SC2086:info:1:6: Doubl
841848
| ^~~~
842849
```
843850

851+
<!-- Skip playground link -->
852+
844853
[shellcheck][] is a famous linter for ShellScript. actionlint runs shellcheck for scripts at `run:` step in a workflow.
845854
For installing shellcheck, see [the official installation document][shellcheck-install].
846855

@@ -959,6 +968,8 @@ test.yaml:23:9: pyflakes reported issue in this script: 1:1: 'time.sleep' import
959968
| ^~~~
960969
```
961970

971+
<!-- Skip playground link -->
972+
962973
Python script can be written in `run:` when `shell: python` is configured.
963974

964975
[pyflakes][] is a famous linter for Python. It is suitable for linting small code like scripts at `run:` since it focuses
@@ -1728,6 +1739,7 @@ jobs:
17281739
```
17291740

17301741
Output:
1742+
<!-- Skip update output -->
17311743

17321744
```
17331745
test.yaml:7:15: missing input "message" which is required by action "My action" defined at "./.github/actions/my-action". all required inputs are "message" [action]
@@ -1740,6 +1752,8 @@ test.yaml:13:11: input "additions" is not defined in action "My action" defined
17401752
| ^~~~~~~~~~
17411753
```
17421754
1755+
<!-- Skip playground link -->
1756+
17431757
When a local action is run in `uses:` of `step:`, actionlint reads `action.yml` file in the local action directory and
17441758
validates inputs at `with:` in the workflow are correct. Missing required inputs and unexpected inputs can be detected.
17451759
@@ -2401,6 +2415,7 @@ jobs:
24012415
```
24022416

24032417
Output:
2418+
<!-- Skip update output -->
24042419

24052420
```
24062421
test.yaml:6:11: input "name" is required by "./.github/workflows/reusable.yaml" reusable workflow [workflow-call]
@@ -2429,6 +2444,8 @@ test.yaml:24:16: input "message" is typed as string by reusable workflow "./.git
24292444
| ^~~~
24302445
```
24312446
2447+
<!-- Skip playground link -->
2448+
24322449
Reusable workflows can define required/optional inputs and secrets. When they are missing or some undefined input is used in a
24332450
workflow call, actionlint reports an error.
24342451
@@ -2480,6 +2497,7 @@ jobs:
24802497
```
24812498
24822499
Output:
2500+
<!-- Skip update output -->
24832501
24842502
```
24852503
test.yaml:13:24: property "tag" is not defined in object type {version: string} [expression]
@@ -2488,6 +2506,8 @@ test.yaml:13:24: property "tag" is not defined in object type {version: string}
24882506
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24892507
```
24902508
2509+
<!-- Skip playground link -->
2510+
24912511
Outputs of workflow call are set to the job's outputs object. They can be accessed by downstream jobs specified with `needs:`.
24922512
What outputs are set is defined in the reusable workflow. actionlint types outputs objects from workflow calls and check the
24932513
object types in downstream jobs.
@@ -2750,19 +2770,6 @@ characters around `${{ }}`.
27502770
<a name="action-metadata-syntax"></a>
27512771
## Action metadata syntax validation
27522772

2753-
Example workflow input:
2754-
2755-
```yaml
2756-
on: push
2757-
2758-
jobs:
2759-
test:
2760-
runs-on: ubuntu-latest
2761-
steps:
2762-
# actionlint checks an action when it is actually used in a workflow
2763-
- uses: ./.github/actions/my-invalid-action
2764-
```
2765-
27662773
Example action metadata:
27672774

27682775
```yaml
@@ -2788,7 +2795,21 @@ runs:
27882795
SOME_VAR: SOME_VALUE
27892796
```
27902797

2798+
Example input:
2799+
2800+
```yaml
2801+
on: push
2802+
2803+
jobs:
2804+
test:
2805+
runs-on: ubuntu-latest
2806+
steps:
2807+
# actionlint checks an action when it is actually used in a workflow
2808+
- uses: ./.github/actions/my-invalid-action
2809+
```
2810+
27912811
Output:
2812+
<!-- Skip update output -->
27922813

27932814
```
27942815
action_metadata_syntax_validation.yaml:8:15: description is required in metadata of "My action" action at "path/to/.github/actions/my-invalid-action/action.yml" [action]
@@ -2817,6 +2838,8 @@ action_metadata_syntax_validation.yaml:8:15: "env" is not allowed in "runs" sect
28172838
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
28182839
```
28192840
2841+
<!-- Skip playground link -->
2842+
28202843
All actions require a metadata file `action.yml` or `aciton.yaml`. The syntax is defined in [the official document][action-metadata-doc].
28212844
28222845
actionlint checks metadata files used in workflows and reports errors when they are not following the syntax.

scripts/update-checks-doc/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/update-checks-doc

scripts/update-checks-doc/main.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"compress/zlib"
7+
"encoding/base64"
8+
"errors"
9+
"fmt"
10+
"log"
11+
"os"
12+
"strings"
13+
14+
"github.com/rhysd/actionlint"
15+
)
16+
17+
func generatePermalink(src []byte) (string, error) {
18+
var out bytes.Buffer
19+
20+
b64 := base64.NewEncoder(base64.StdEncoding, &out)
21+
comp, _ := zlib.NewWriterLevel(b64, zlib.BestCompression)
22+
23+
scan := bufio.NewScanner(bytes.NewReader(src))
24+
first := true
25+
for scan.Scan() {
26+
l := scan.Bytes()
27+
if bytes.HasPrefix(bytes.TrimSpace(l), []byte("#")) {
28+
continue
29+
}
30+
if first {
31+
first = false
32+
} else {
33+
comp.Write([]byte{'\n'})
34+
}
35+
comp.Write(l)
36+
}
37+
if err := scan.Err(); err != nil {
38+
return "", err
39+
}
40+
41+
comp.Close()
42+
b64.Close()
43+
44+
return fmt.Sprintf("[Playground](https://rhysd.github.io/actionlint#%s)", out.Bytes()), nil
45+
}
46+
47+
func runActionlint(src []byte) ([]byte, error) {
48+
var out bytes.Buffer
49+
50+
opts := &actionlint.LinterOptions{
51+
StdinFileName: "test.yaml",
52+
Shellcheck: "shellcheck",
53+
Pyflakes: "pyflakes",
54+
Color: actionlint.ColorOptionKindNever,
55+
}
56+
57+
l, err := actionlint.NewLinter(&out, opts)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
if _, err := l.Lint("test.yaml", src, nil); err != nil {
63+
return nil, err
64+
}
65+
66+
return out.Bytes(), nil
67+
}
68+
69+
func update(in []byte) ([]byte, error) {
70+
var buf bytes.Buffer
71+
72+
var input bytes.Buffer
73+
var anchor string
74+
var section string
75+
var inputHeader bool
76+
var outputHeader bool
77+
var inInput bool
78+
var count int
79+
lnum := 0
80+
scan := bufio.NewScanner(bytes.NewReader(in))
81+
for scan.Scan() {
82+
lnum++
83+
l := scan.Text()
84+
if strings.HasPrefix(l, "## ") && anchor != "" {
85+
if input.Len() > 0 {
86+
return nil, fmt.Errorf("example input for %q was not consumed. playground link may not exist", section)
87+
}
88+
if section != "" && count == 0 {
89+
log.Printf("Section %q contains NO example", section)
90+
}
91+
section = l[3:]
92+
log.Printf("Entering new section %q (%s) at line %d", section, anchor, lnum)
93+
anchor = ""
94+
inputHeader = false
95+
outputHeader = false
96+
inInput = false
97+
count = 0
98+
}
99+
if strings.HasPrefix(l, `<a name="`) && strings.HasSuffix(l, `"></a>`) {
100+
anchor = strings.TrimSuffix(strings.TrimPrefix(l, `<a name="`), `"></a>`)
101+
}
102+
if l == "Example input:" {
103+
log.Printf("Found example input header for %q at line %d", section, lnum)
104+
inputHeader = true
105+
}
106+
if l == "```yaml" && inputHeader {
107+
inputHeader = false
108+
inInput = true
109+
} else if inInput && l != "```" {
110+
input.WriteString(l)
111+
input.WriteByte('\n')
112+
}
113+
if l == "```" {
114+
if inInput {
115+
log.Printf("Found an input example (%d bytes) for %q at line %d", input.Len(), section, lnum)
116+
inInput = false
117+
} else if outputHeader {
118+
buf.WriteString("```\n")
119+
for {
120+
if !scan.Scan() {
121+
return nil, fmt.Errorf("code block for output of %q does not close", section)
122+
}
123+
lnum++
124+
if scan.Text() == "```" {
125+
break
126+
}
127+
}
128+
if input.Len() == 0 {
129+
return nil, fmt.Errorf("output cannot be generated because example input for %q does not exist", section)
130+
}
131+
log.Printf("Generating output for the input example for %q at line %d", section, lnum)
132+
out, err := runActionlint(input.Bytes())
133+
if err != nil {
134+
return nil, err
135+
}
136+
buf.Write(out)
137+
}
138+
}
139+
if l == "Output:" {
140+
log.Printf("Found example output header for %q at line %d", section, lnum)
141+
outputHeader = true
142+
}
143+
if l == "<!-- Skip update output -->" {
144+
log.Printf("Skip updating output for %q due to the comment at line %d", section, lnum)
145+
outputHeader = false
146+
}
147+
if strings.HasPrefix(l, "[Playground](https://rhysd.github.io/actionlint#") && strings.HasSuffix(l, ")") {
148+
if input.Len() == 0 {
149+
return nil, fmt.Errorf("playground link cannot be generated because example input for %q does not exist", section)
150+
}
151+
if !outputHeader {
152+
return nil, fmt.Errorf("output code block is missing for %q", section)
153+
}
154+
link, err := generatePermalink(input.Bytes())
155+
if err != nil {
156+
return nil, err
157+
}
158+
buf.WriteString(link)
159+
buf.WriteByte('\n')
160+
log.Printf("Generate playground link for %q at line %d: %s", section, lnum, link)
161+
outputHeader = false
162+
input.Reset()
163+
count++
164+
continue
165+
}
166+
if l == "<!-- Skip playground link -->" {
167+
log.Printf("Skip generating playground link for %q due to the comment at line %d", section, lnum)
168+
outputHeader = false
169+
input.Reset()
170+
count++
171+
}
172+
buf.WriteString(l)
173+
buf.WriteByte('\n')
174+
}
175+
return buf.Bytes(), scan.Err()
176+
}
177+
178+
func Main(args []string) error {
179+
if len(os.Args) < 2 {
180+
return errors.New("usage: update-checks-doc FILE")
181+
}
182+
p := os.Args[1]
183+
in, err := os.ReadFile(p)
184+
if err != nil {
185+
return err
186+
}
187+
log.Printf("Read %d bytes from %q", len(in), p)
188+
189+
out, err := update(in)
190+
if err != nil {
191+
return err
192+
}
193+
194+
if bytes.Equal(in, out) {
195+
log.Printf("Do nothing because there is no update in %q", p)
196+
return nil
197+
}
198+
199+
log.Printf("Generate the updated content (%d bytes) for %q", len(out), p)
200+
if err := os.WriteFile(p, out, 0666); err != nil {
201+
return err
202+
}
203+
204+
return nil
205+
}
206+
207+
func main() {
208+
if err := Main(os.Args); err != nil {
209+
fmt.Fprintln(os.Stderr, err)
210+
os.Exit(1)
211+
}
212+
}

0 commit comments

Comments
 (0)