Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Lint actions #366

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open

feat: Lint actions #366

wants to merge 26 commits into from

Conversation

msw-kialo
Copy link

This PR tries to address #46 to lint action metadata files (action.yml / action.yaml) analog to workflow files. In addition to scheme validation this allows finding more sophisticated errors by running pyflake, shellcheck, validation expressions and other cross field references (e.g. outputs). The main advantage is obviously for people writing composite actions.

Included changes

This PR tries to cover all essential aspects of this feature and provides example implementation for most of them:

  • Add new parse* methods to parse action metadata.
  • Add a new input-format parameter (both to the CLI and linteropts) to decide whether the input should be treated as workflow file or action metadata file.
  • Adapt rules if needed to handle action metadata files, too.
  • Add new branding rule to validate branding values.
  • Adapt the playground to allow linting of actions, too.
  • Add tests to correctly valid common actions and caught various issues.

Implementation decisions

We needed to take a few implementation decisions where it is not clear whether to picked the best option (once they are decided we will squash our changes and mark the PR as ready):

  • API stability: the public interface was kept unchanged unchanged (if needed function alias like Parse and Visit were added). At the moment, the visitors pass interface is extended by VisitActionPre and VisitActionPost, however it reuses visitStep. This could "crash" (nil value accessed) custom rules as VisitWorkflowPre, VisitJobPre might not be called before visitStep: the RuleID, RuleExpression had this issue.
  • parseAction vs ActionMetadata: The local action metadata cache (action_metadata.go) was left unchanged (it appears to be more light-weight and overall simpler to leave as small duplication).
  • LintDir behavior: to keep the change simple, LintDir will only lint arbitrary .yml, .yaml files if the directory is .github/workflows. This should be an issue in practice (as workflows needs to be in such directory to be interpreted by GitHub), but it required the testdata/projects tests to be adapted (specify -input-format=workflow would also work).
  • Default behavior: actions are linted by default (at least if actionlint is invoked without extra arguments)
  • Action location: At the moment, any action.yml / action.yaml file within the repository is considered an action metadata file (top level, or in any subdirectory at any level). While it ensures by default any such file is discovered, it might slow if it is a big repository and could cause false positives (the filename isn't that specific). Discovery could be reduced; but, new config option might be needed to handle more complex use-cases. -input-format=workflow was added to the dog-food CI calls to ensure the testdata actions aren't discovered.
  • CLI shortcut: this PR added actionlint $dir to lint only files within the specific directory, e.g. actionlint .github/actions (it was originally added for testing purposes but it might be helpful for others, too).
  • Expression values: Sadly, we haven't found anything in the GitHub documentation which expressions are supported action metadata files (probably the step files are the same, however, inputs.X.default, runs.envs.X, runs.pre-if, runs.post-if are new. E.g. I suspect special functions like always(), success() are supported for runs.pre-if but I can't proof it.
  • Branding values: the allowed branding values (color and icon) are currently hardcoded. I suspect, a script to extracted them from GitHub's docs should be added?

msw-kialo and others added 26 commits October 9, 2023 11:15
For the moment we kept the old scheme and rules.
It allows existing rules to function as are (they potentially make
preparation and post actions in Workflow* Job* functions).
">" stricts newlines with single spaces, but apparently this isn't
picked by actionlint/shellcheck
Starting to port rule_id, rule_expression and rule_shellcheck to
correctly handle action visits.
Use interface for the Runs block and a normal struct for the Action itself
Select syntax to check based on file name or top-level yaml keys.
@msw-kialo msw-kialo marked this pull request as ready for review October 30, 2023 14:01
@msw-kialo
Copy link
Author

Marking PR as ready to indicate further work depends on PR review.

@Ezri-Mudde
Copy link

What's the status on this? Will this eventually be merged or should I look for another tool to lint action files? I've tried @msw-kialo changes on my action files and it seems to work perfectly. I would like to use this in my actions workflow without having to clone and build their fork.

@msw-kialo
Copy link
Author

I am ready to make needed adjustments, rebases, restructure the PR/change etc.
However, I am waiting for a first review from @rhysd before I invest more time in a potentially wrong direction.

Copy link
Contributor

@ChrisCarini ChrisCarini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, LGTM! 🎉 👏

Left several suggestions.

I also tested this branch locally against ~10 workflows and ~50 actions after I merged in the latest changes (kialo:lint-actions is about 250 commits behind main right now; a few trivial merge-conflicts to resolve).

Comment on lines +949 to +950
// ActionRuns is an interface how the action is executed. Action can be executed as JavaScript, Docker or composite steps.
type ActionRuns interface {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// ActionRuns is an interface how the action is executed. Action can be executed as JavaScript, Docker or composite steps.
type ActionRuns interface {
// ActionRuns is an interface how the action is executed. Action can be executed as JavaScript, Docker or composite steps.
// https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs
type ActionRuns interface {

@@ -77,6 +78,15 @@ type Command struct {
Stderr io.Writer
}

func isDir(path string) bool {
// use a switch to make it a bit cleaner
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// use a switch to make it a bit cleaner
// TODO: use a switch to make it a bit cleaner

todo or remove? (leaving suggestions for both; you pick? 😄)

@@ -77,6 +78,15 @@ type Command struct {
Stderr io.Writer
}

func isDir(path string) bool {
// use a switch to make it a bit cleaner
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// use a switch to make it a bit cleaner

Comment on lines +275 to +281
var parentDir string
if l.inputFormat == FileWorkflow {
parentDir = p.WorkflowsDir()
} else {
parentDir = p.RootDir()
}
return l.LintDir(parentDir, p)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not able to suggest across all needed lines, but can this whole function turn into just:

Suggested change
var parentDir string
if l.inputFormat == FileWorkflow {
parentDir = p.WorkflowsDir()
} else {
parentDir = p.RootDir()
}
return l.LintDir(parentDir, p)
var parentDir string
if l.inputFormat == FileWorkflow {
parentDir = p.WorkflowsDir()
} else {
parentDir = p.RootDir()
}
return l.LintDirInRepository(parentDir, p)

(and, delete lines 264-274 above, which looks like duplicate w/ everything in LintDirInRepository)

}
if a != nil {
if err := v.VisitAction(a); err != nil {
l.debug("error occurred while visiting workflow syntax tree: %v", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
l.debug("error occurred while visiting workflow syntax tree: %v", err)
l.debug("error occurred while visiting action syntax tree: %v", err)

return format
}
if strings.Contains(filename, ".github/workflows/") {
// println("Detect", filename, "as workflow file based (due to its directory)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth having this as a debug statement?

return FileWorkflow
}
if strings.HasSuffix(filename, "/action.yaml") || strings.HasSuffix(filename, "/action.yml") {
// println("Detect", filename, "as action filename based (due to its filename)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth having this as a debug statement?


// selecting Action if `runs` element is present Workflow otherwise:
if isNull(node) || len(node.Content) == 0 || node.Content[0].Kind != yaml.MappingNode {
// println("Defaulted", filename, "as workflow (file is not a yaml mapping)", nodeKindName(node.Kind))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth having this as a debug statement?


for i := 0; i < len(node.Content[0].Content); i += 2 {
if node.Content[0].Content[i].Kind == yaml.ScalarNode && node.Content[0].Content[i].Value == "runs" {
// println("Detected", filename, "as action workflow (it has a 'runs' key)")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth having this as a debug statement?

Comment on lines +53 to +54
rule.seen = nil
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rule.seen = nil
return nil
rule.seen = nil
return nil

@ChrisCarini
Copy link
Contributor

  • Branding values: the allowed branding values (color and icon) are currently hardcoded. I suspect, a script to extracted them from GitHub's docs should be added?

I'm not sure if GitHub themselves provides anything, but there is a JSON payload that has the respective values here: #366

Honestly, I don't think these change often enough that it warrants the added complexity in this initial PR. My 2 cents is this can be tracked as a future feature improvement, and implemented if/when it becomes an issue for folks down the road.

@ChrisCarini
Copy link
Contributor

Oh @msw-kialo another thing I forgot to comment on - it's worth updating the README.md as part of this change, since it's very significant, and worth highlighting this new functionality in the README. What do you think?

@msw-kialo
Copy link
Author

msw-kialo commented Jul 1, 2024

@ChrisCarini Thanks for testing it and providing feedback.

I am still open to make needed changes to this PR. However, since https://github.com/rhysd/actionlint/releases/tag/v1.7.0:

  • From this version, actionlint starts to check action metadata file action.yml (or action.yaml). At this point, only very basic checks are implemented and contents of steps: are not checked yet.
    • Checks for steps: contents are planned to be implemented. Since several differences are expected between steps: in workflow file and steps: in action metadata file (e.g. available contexts), the implementation is delayed to later version. And the current implementation of action metadata parser is ad hoc. I'm planning a large refactorying and breaking changes Go API around it are expected.

It appears that @rhysd has other architecture plans. So until I hear where this is going I won't invest more time into this. Actually, I consider just closing this PR as it is apparently not appreciated.

@ChrisCarini
Copy link
Contributor

@ChrisCarini Thanks for testing it and providing feedback.

Hi @msw-kialo - no problem! Thank you for implementing this functionality and posting the PR! 🙏

I am still open to make needed changes to this PR. However, since https://github.com/rhysd/actionlint/releases/tag/v1.7.0: ...

It appears that @rhysd has other architecture plans. So until I hear where this is going I won't invest more time into this. Actually, I consider just closing this PR as it is apparently not appreciated.

I understand that's a bit frustrating :( FWIW, I would actually really love it if your fork for this PR was released separately until this repo either (a) decides to provide further comment in pursuit of getting the feature merged in, or (b) decides to implement this support some other way.

Ultimately, I really just want this functionality (and personally don't mind who/where it is implemented and released) so I can start using faster.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants