Skip to content

bitwarden/workflow-linter

Repository files navigation

Bitwarden Workflow Linter

Bitwarden's Workflow Linter is an extensible linter to apply opinionated organization-specific GitHub Action standards. It was designed to be used alongside tools like action-lint and yamllint to check for correct Action syntax and enforce specific YAML standards.

To see an example of Workflow Linter in practice in GitHub Action, see the composite Action.

Installation

From GitHub Release

Not yet implemented

Locally

git clone git@github.com:bitwarden/workflow-linter.git
cd workflow-linter

pip install -e .

Usage

Setup settings.yaml

If a non-default configuration is desired (different than src/bitwarden_workflow_linter/default_settings.yaml), copy the below and create a settings.yaml in the directory that bwwl will be running from.

enabled_rules:
    - bitwarden_workflow_linter.rules.name_exists.RuleNameExists
    - bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized
    - bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned
    - bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix
    - bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned
    - bitwarden_workflow_linter.rules.underscore_outputs.RuleUnderscoreOutputs

approved_actions_path: default_actions.json
usage: bwwl [-h] [-v] {lint,actions} ...

positional arguments:
  {lint,actions}
    lint          Verify that a GitHub Action Workflow follows all of the Rules.
    actions       Add or Update Actions in the pre-approved list.

options:
  -h, --help      show this help message and exit
  -v, --verbose

Development

Requirements

  • Python 3.11
  • pipenv

Setup

pipenv install --dev
pipenv shell

Testing

All built-in src/bitwarden_workflow_linter/rules should have 100% code coverage and we should shoot for an overall coverage of 80%+. We are lax on the imperative shell (code interacting with other systems; ie. disk, network, etc), but we strive to maintain a high coverage over the functional core (objects and models).

pipenv shell
pytest tests --cov=src

Code Reformatting

We adhere to PEP8 and use black to maintain this adherence. black should be run on any change being merged to main.

pipenv shell
black .

Linting

We loosely use Google's Python style guide, but yield to black when there is a conflict

pipenv shell
pylint --rcfile pylintrc src/ tests/

Add a new Rule

A new Rule is created by extending the Rule base class and overriding the fn(obj: Union[Workflow, Job, Step]) method. Available attributes of Workflows, Jobs and Steps can be found in their definitons under src/models.

For a simple example, we'll take a look at enforcing the existence of the name key in a Job. This is already done by default with the src.rules.name_exists.RuleNameExists, but provides a simple enough example to walk through.

from typing import Union, Tuple

from ..rule import Rule
from ..models.job import Job
from ..models.workflow import Workflow
from ..models.step import Step
from ..utils import LintLevels, Settings


class RuleJobNameExists(Rule):
    def __init__(self, settings: Settings = None) -> None:
        self.message = "name must exist"
        self.on_fail: LintLevels = LintLevels.ERROR
        self.compatibility: List[Union[Workflow, Job, Step]] = [Job]
        self.settings: Settings = settings

    def fn(self, obj: Job) -> Tuple[bool, str]:
        """<doc block goes here> """
        if obj.name is not None:
            return True, ""
        return False, self.message

By default, a new Rule needs five things:

  • self.message: The message to return to the user on a lint failure
  • self.on_fail: The level of failure on a lint failure (NONE, WARNING, ERROR). NONE and WARNING will exit with a code of 0 (unless using strict mode for WARNING). ERROR will exit with a non-zero exit code
  • self.compatibility: The list of objects this rule is compatible with. This is used to create separate instances of the Rule for each object in the Rules collection.
  • self.settings: In general, this should default to what is shown here, but allows for overrides
  • self.fn: The function doing the actual work to check the object and enforce the standard.

fn can be as simple or as complex as it needs to be to run a check on a single object. This linter currently does not support Rules that check against multiple objects at a time OR file level formatting (one empty between each step or two empty lines between each job)

To activate a rule after implementing it, add it to settings.yaml in the project's base folder and src/bitwarden_workflow_linter/default_settings.yaml to make the rule default

ToDo

  • Add Rule to assert correct format for single line run