Metadata-Version: 2.5
Name: greenwash
Version: 0.1.0
Summary: Find required CI checks that can go green without doing the work.
Project-URL: Homepage, https://github.com/aviseth/greenwash
Project-URL: Issues, https://github.com/aviseth/greenwash/issues
Author: Avi Seth
License: MIT License
        
        Copyright (c) 2026 Avi Seth
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: audit,branch-protection,ci,github-actions
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Requires-Dist: pyyaml>=6.0
Description-Content-Type: text/markdown

# greenwash

Find required CI checks that can go green without doing the work.

[![PyPI](https://img.shields.io/pypi/v/greenwash.svg)](https://pypi.org/project/greenwash/)
[![Python](https://img.shields.io/pypi/pyversions/greenwash.svg)](https://pypi.org/project/greenwash/)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

A green required check is treated as proof the tests ran. It is not. From
[GitHub's own documentation](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches):

> Required status checks must have a `successful`, `skipped`, or `neutral` status
> before collaborators can make changes to a protected branch.

Skipped counts as passed. So a job gated behind an `if:` that evaluated false, a
job skipped because the job it needed failed, a workflow filtered out by `paths:`
and backstopped by a same-named always-green twin, or a step wrapped in
`continue-on-error: true` all produce the same green tick as a full passing run.
Nothing in the UI tells them apart. The merge succeeds, `main` breaks, and the
pull request still shows all-green months later.

greenwash reads your branch rules and your workflow definitions and tells you
which of your gates are real.

## Installation

```shell
pip install greenwash
```

Requires Python 3.10+ and a GitHub token, from `GITHUB_TOKEN`, `GH_TOKEN`, or
`gh auth login`.

## Quick start

```shell
greenwash audit                       # the repository you are standing in
greenwash audit --repo owner/name
```

```text
$ greenwash audit --repo home-assistant/core
home-assistant/core (dev)  8 required checks, 13 workflows, via ruleset

2 hollow, 5 blocked, 2 advisory

[hollow] Check all requirements  (skips-with-upstream)
  where   .github/workflows/ci.yaml:gen-requirements-all
  detail  needs: info, base
  why     if an upstream job fails or is skipped this job is skipped too,
          which satisfies the gate rather than failing it -- the failure
          merges
```

Three severities:

- **hollow** — the check can report success without the work running.
- **blocked** — the check can never report at all, so the gate is stale or the
  branch is unmergeable.
- **advisory** — the signal is weakened but not void, or the finding depends on
  a choice you may have made deliberately.

## What it looks for

| Finding | Severity | What it means |
|---|---|---|
| `skippable-condition` | hollow | The job's `if:` can be false, and a skip satisfies the gate |
| `skips-with-upstream` | hollow | An unguarded `needs:` — when the upstream fails, this job skips and the failure merges |
| `aggregate-gate-without-check` | hollow | A job running under `always()` that never inspects `needs.*.result`, so it passes when everything it gates on failed |
| `continue-on-error-job` | hollow | The job reports success even when its steps fail |
| `duplicate-check-name` | hollow | Two or more jobs that can fire on a pull request report under the same name |
| `required-check-has-no-job` | blocked | Nothing in any workflow reports under this name |
| `aggregate-gate-allows-skips` | advisory | `alls-green` with `allowed-skips`, so a green tick does not prove those jobs ran |
| `continue-on-error-step` | advisory | A step can fail without failing the check |
| `escape-hatch-step` | advisory | `\|\| true` or `set +e` discards a command's failure inside the shell |
| `path-filtered-workflow` | advisory | A `paths:` filter means the workflow may not run for some pull requests |

## In CI

```shell
greenwash audit --fail-on hollow
```

Exit 1 when any hollow finding exists, so a repository can gate on its own gates
being real. `--fail-on any` includes advisories. `--json` gives the same data
machine-readable.

## What it does not do

**It cannot tell you whether a job that ran did any real work.** A job whose
`pytest` step collected zero tests looks identical to one that ran the suite.
This is a static audit of workflow definitions against branch rules; proving the
work happened needs run history and a declared witness, which is not built yet.

**It cannot read what your token cannot read.** Classic branch protection needs
admin. When a branch reports `protected: true` and no rules come back, greenwash
says so and stops rather than reporting a clean bill of health — a tool that
called an invisible gate a missing one would be committing the error it exists
to find.

**It does not resolve reusable workflows.** A `uses:` workflow reports as
`caller / job`, which this version does not follow, so those may show up as
`required-check-has-no-job`. Third-party apps that post check runs will too.

**Merge queues change when the gate applies.** A job gated on `merge_group` is
skipped on the pull request and runs in the queue. greenwash reports the
definition; whether that is a problem depends on where you enforce merging.

## Why the name

Greenwashing is presenting something as better than it is on the strength of a
symbol rather than the substance behind it. A green tick that certifies nothing
is the same trick.

## License

MIT
