detached 98594d0

lint.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package action
4
5import (
6	"bnema.dev/ermite-actions/expr"
7	"bnema.dev/ermite-actions/template"
8	"bnema.dev/ermite-actions/workflow"
9)
10
11// strictRootType is the language service's definition for an editor.
12const strictRootType = "action-root-strict"
13
14// Lint reports what the language service (@actions/workflow-parser
15// parseAction and convertActionTemplate, the code behind an editor's
16// squiggles) finds wrong in an action.yml, and returns no model.
17//
18// It reads the file against "action-root-strict": name, description and runs
19// are required, "using" must be exactly one of docker, node12, node16, node20,
20// node24 or composite, "branding" is checked, and unknown top-level keys such
21// as "icon" and "color" and a "plugin" action are refused. GitHub's runner
22// accepts several of those; [Parse] says what GitHub does, Lint what a linter
23// should flag. The conversion adds the errors of the conditions: pre-if and
24// post-if, and the "if" of composite steps, are parsed with the contexts the
25// schema grants (a step condition may not use vars or needs here, though the
26// runner lets it).
27//
28// The messages are the language service's: positioned, the expression errors
29// without the runner's ". Located at position" suffix being the one
30// difference of ours (package expr follows the runner). YAML syntax errors
31// have yaml.v3's wording.
32func Lint(path string, data []byte) []Error {
33	sch, err := Schema()
34	if err != nil {
35		return []Error{{Raw: "internal error: " + err.Error()}}
36	}
37	ctx := template.NewContext(sch)
38	root := template.ReadYAML(ctx, strictRootType, path, data)
39	if !ctx.HasErrors() && root != nil {
40		lintConditions(ctx, sch, root)
41	}
42	return collect(ctx)
43}
44
45// lintConditions is validateRunsIfCondition for pre-if and post-if and
46// convertToIfCondition for the "if" of every step of "runs.steps".
47func lintConditions(ctx *template.Context, sch *template.Schema, root template.Token) {
48	m, ok := root.(*template.Mapping)
49	if !ok {
50		return
51	}
52	runs, ok := m.Find("runs").(*template.Mapping)
53	if !ok {
54		return
55	}
56	runsIf := sch.Info("runs-if")
57	stepIf := sch.Info("step-if")
58	for _, p := range runs.Pairs {
59		switch p.Key.String() {
60		case "pre-if", "post-if":
61			if s, ok := p.Value.(*template.String); ok {
62				if _, err := expr.Parse(s.Value, runsIf.NamedContexts(), runsIf.Functions()); err != nil {
63					ctx.ErrorAt(s, err.Error())
64				}
65			}
66		case "steps":
67			seq, ok := p.Value.(*template.Sequence)
68			if !ok {
69				continue
70			}
71			for _, it := range seq.Items {
72				if sm, ok := it.(*template.Mapping); ok {
73					lintStepIf(ctx, stepIf, sm.Find("if"))
74				}
75			}
76		}
77	}
78}
79
80func lintStepIf(ctx *template.Context, def *template.DefinitionInfo, tok template.Token) {
81	var condition string
82	switch s := tok.(type) {
83	case nil:
84		return
85	case *template.BasicExpression:
86		condition = s.Expression
87	case *template.String:
88		condition = s.Value
89	default:
90		return
91	}
92	if isNullOrWhiteSpace(condition) {
93		return
94	}
95	// Condition text is trimmed for a string; an expression is used as it is.
96	if _, isString := tok.(*template.String); isString {
97		condition = trimSpaceJS(condition)
98	}
99	final := condition
100	if node, err := expr.Parse(condition, def.NamedContexts(), def.Functions()); err == nil && !workflow.HasStatusFunction(node) {
101		final = "success() && (" + condition + ")"
102	}
103	if _, err := expr.Parse(final, def.NamedContexts(), def.Functions()); err != nil {
104		ctx.ErrorAt(tok, err.Error())
105	}
106}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close