{"id":20675564,"url":"https://github.com/bitwarden/workflow-linter","last_synced_at":"2026-05-05T14:02:30.846Z","repository":{"id":241968129,"uuid":"808281404","full_name":"bitwarden/workflow-linter","owner":"bitwarden","description":null,"archived":false,"fork":false,"pushed_at":"2026-03-27T15:48:45.000Z","size":4259,"stargazers_count":4,"open_issues_count":8,"forks_count":3,"subscribers_count":16,"default_branch":"main","last_synced_at":"2026-03-28T17:54:12.099Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bitwarden.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-05-30T18:38:02.000Z","updated_at":"2026-03-03T21:07:01.000Z","dependencies_parsed_at":"2024-05-30T23:58:55.014Z","dependency_job_id":"80348634-09fb-4c7e-9d92-283dce323341","html_url":"https://github.com/bitwarden/workflow-linter","commit_stats":null,"previous_names":["bitwarden/workflow-linter"],"tags_count":132,"template":false,"template_full_name":"bitwarden/template","purl":"pkg:github/bitwarden/workflow-linter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitwarden%2Fworkflow-linter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitwarden%2Fworkflow-linter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitwarden%2Fworkflow-linter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitwarden%2Fworkflow-linter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bitwarden","download_url":"https://codeload.github.com/bitwarden/workflow-linter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bitwarden%2Fworkflow-linter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31335219,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-03T04:42:29.251Z","status":"ssl_error","status_checked_at":"2026-04-03T04:42:12.667Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-16T21:09:50.883Z","updated_at":"2026-05-05T14:02:30.832Z","avatar_url":"https://github.com/bitwarden.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Bitwarden Workflow Linter\n\nBitwarden's Workflow Linter is an extensible linter to apply opinionated organization-specific GitHub Action standards. It was designed to be used alongside [yamllint](https://github.com/adrienverge/yamllint) to enforce specific YAML standards.\n\nTo see an example of Workflow Linter in practice in GitHub Action, see the [composite Action](https://github.com/bitwarden/gh-actions/tree/main/lint-workflow).\n\n## Prerequisites\n\n- Python 3.13\n- pipenv\n- Windows systems: Chocolatey package manager\n- Mac OS systems: Homebrew package manager\n- pipx\n\n\u003e [!NOTE]\n\u003e Python 3.12 is compatible but Python 3.13 is encouraged.\n\n## Setup\n\n1. **Create the virtual environment:**\n   ```bash\n   python3.13 -m venv /Users/$USER/bitwarden_workflow_linter_venv\n   ```\n\n2. **Activate the virtual environment:**\n   ```bash\n   source /Users/$USER/bitwarden_workflow_linter_venv/bin/activate\n   ```\n\n## Installation\n\n### From PyPI\nThis is the recommended method for most users. Installing from PyPI ensures you get the latest stable release and is the easiest way to install and update the package.\n\n\n1. **Install Bitwarden Workflow Linter:**\n   ```bash\n   pip install --upgrade bitwarden_workflow_linter\n   ```\n\n2. **Deactivate the virtual environment (optional):**\n   ```bash\n   deactivate\n   ```\n#### Using pipx\n\nAlternatively, you can install `bwwl` globally using `pipx` to keep it isolated:\n\n1. **Install Bitwarden Workflow Linter:**\n   ```bash\n   pipx install bitwarden_workflow_linter --python python3.13\n   ```\n\nThis method is ideal for running `bwwl` as a standalone CLI tool without managing a virtual environment manually.\n\n### From GitHub Release\nUse this method if you need a specific version of the package that is not yet available on PyPI, or if you want to access pre-release versions.\n\n1. **Download the release tarball or zip file from GitHub:**\n   ```bash\n   wget https://github.com/bitwarden/workflow-linter/archive/refs/tags/vX.Y.Z.tar.gz\n   tar -xzf vX.Y.Z.tar.gz\n   cd workflow-linter-X.Y.Z\n   ```\n\n2. **Install the package:**\n   ```bash\n   pip install .\n   ```\n\n3. **Deactivate the virtual environment (optional):**\n   ```bash\n   deactivate\n   ```\n\n### Locally\nThis method is useful for developers who want to contribute to the project or need to make local modifications to the source code. *Make sure to follow the virtual environment prerequisite setup*\n1. **Clone the repository:**\n   ```bash\n   git clone git@github.com:bitwarden/workflow-linter.git\n   cd workflow-linter\n   ```\n\n2. **Install the package:**\n   ```bash\n   pip install -e .\n   ```\n\n3. **Deactivate the virtual environment (optional):**\n   ```bash\n   deactivate\n   ```\n\n## Usage\n\n### Setup settings.yaml\n\nIf 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 ( generally will be the root directory in CI).\n\n```yaml\nenabled_rules:\n    - id: bitwarden_workflow_linter.rules.name_exists.RuleNameExists\n      level: error\n    - id: bitwarden_workflow_linter.rules.name_capitalized.RuleNameCapitalized\n      level: error\n    - id: bitwarden_workflow_linter.rules.pinned_job_runner.RuleJobRunnerVersionPinned\n      level: error\n    - id: bitwarden_workflow_linter.rules.job_environment_prefix.RuleJobEnvironmentPrefix\n      level: error\n    - id: bitwarden_workflow_linter.rules.step_approved.RuleStepUsesApproved\n      level: error\n    - id: bitwarden_workflow_linter.rules.step_pinned.RuleStepUsesPinned\n      level: error\n    - id: bitwarden_workflow_linter.rules.underscore_outputs.RuleUnderscoreOutputs\n      level: warning\n    - id: bitwarden_workflow_linter.rules.run_actionlint.RunActionlint\n      level: warning\n    - id: bitwarden_workflow_linter.rules.check_pr_target.RuleCheckPrTarget\n      level: error\n    - id: bitwarden_workflow_linter.rules.permissions_exist.RulePermissionsExist\n      level: warning\n\napproved_actions_path: default_actions.json\ndefault_branch: main\n```\n\n### Command Line Usage\n\n```bash\nusage: bwwl [-h] [-v] {lint,actions} ...\n\npositional arguments:\n  {lint,actions}\n    lint          Verify that a GitHub Action Workflow follows all of the Rules.\n    actions       Add or Update Actions in the pre-approved list.\n\noptions:\n  -h, --help      show this help message and exit\n  -v, --verbose\n```\n\n#### lint subcommand\n\n```bash\nusage: bwwl lint [-h] [-s | -e] -f FILES [FILES ...] [-o OUTPUT]\n\noptions:\n  -h, --help            show this help message and exit\n  -s, --strict          return non-zero exit code on warnings as well as errors\n  -e, --errors-only     only show and fail on errors; warnings are suppressed\n                        from output and do not affect the exit code\n  -f, --files FILES     files or directories to lint\n  -o, --output OUTPUT   output format: [stdout|json|md] (default: stdout)\n```\n\n\u003e **Note:** `--strict` and `--errors-only` are mutually exclusive.\n## Pre-commit Hook Setup\n\n### Navigate to the `.git/hooks` directory in the repository you wish to lint:\n\n```bash\ncd .git/hooks\n```\n\n### Create the `pre-commit` file (if it does not already exist):\n\n```bash\ntouch pre-commit\n```\n\n### Make the script executable:\n\n```bash\nchmod +x pre-commit\n```\n\n### Edit the `pre-commit` script:\n\nOpen the `pre-commit` file with your favorite text editor and add the following content, replacing `/Users/$USER/bitwarden_workflow_linter_venv/bin/activate` with the actual path to your virtual environment:\n\n```bash\n#!/bin/bash\nset -e\n# Activate the virtual environment\nsource \"/Users/$USER/bitwarden_workflow_linter_venv/bin/activate\"\n# Get the repository root directory\nrepo_root=$(git rev-parse --show-toplevel)\n# Run your Python script\nbwwl lint -f \"$repo_root/.github/workflows\"\n# Deactivate the virtual environment (optional)\ndeactivate\n```\n\n### Test the Hook:\n\nTry committing a change to the repository. The pre-commit hook should run the workflow linter.\n\n## Development\n\n### Setup\nRefer to the [Locally](#locally) instructions above to clone the repository and install the package.\n\n### Testing\n\nAll 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](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-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).\n\n```bash\npipenv shell\npytest tests --cov=src\n```\n\n### Code Reformatting\n\nWe adhere to PEP8 and use `black` to maintain this adherence. `black` should be run on any change being merged to `main`.\n\n```bash\npipenv shell\nblack .\n```\n\n### Linting\n\nWe loosely use [Google's Python style guide](https://google.github.io/styleguide/pyguide.html), but yield to `black` when there is a conflict.\n\n```bash\npipenv shell\npylint --rcfile pylintrc src/ tests/\n```\n\n### Add a new Rule\n\nA 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 definitions under `src/models`.\n\nFor 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.\n\n```python\nfrom typing import Union, Tuple\n\nfrom ..rule import Rule\nfrom ..models.job import Job\nfrom ..models.workflow import Workflow\nfrom ..models.step import Step\nfrom ..utils import LintLevels, Settings\n\n\nclass RuleJobNameExists(Rule):\n    def __init__(self, settings: Settings = None, lint_level: Optional[LintLevels] = LintLevels.ERROR) -\u003e None:\n        self.message = \"name must exist\"\n        self.on_fail: LintLevels = lint_level\n        self.compatibility: List[Union[Workflow, Job, Step]] = [Job]\n        self.settings: Settings = settings\n\n    def fn(self, obj: Job) -\u003e Tuple[bool, str]:\n        \"\"\"\u003cdoc block goes here\u003e \"\"\"\n        if obj.name is not None:\n            return True, \"\"\n        return False, self.message\n```\n\nBy default, a new Rule needs five things:\n\n- `self.message`: The message to return to the user on a lint failure\n- `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\n- `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.\n- `self.settings`: In general, this should default to what is shown here, but allows for overrides\n- `self.fn`: The function doing the actual work to check the object and enforce the standard.\n\n`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).\n\n_IMPORTANT: A rule must be implemented and tested then merged into `main` before it can be activated._ This is because the released version of `bwwl` will use the current `settings.yaml` file, but it will not have the new rule functionality yet and cause an error in the workflow linting of this repository.\n\nTo activate a rule after implementing and releasing 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.\n\nBefore creating a new rule please read the [Workflow linter rule rollout process](./RULE_ROLLOUT.md) document in which you'll find the process for rolling out new workflow linter rules.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitwarden%2Fworkflow-linter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbitwarden%2Fworkflow-linter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbitwarden%2Fworkflow-linter/lists"}