> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pipefort.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Bulk remediation campaigns

> Open auto-fix pull requests across many repositories at once and track every PR from one place.

Fixing findings one repo at a time doesn't scale past a handful of
repositories. A **bulk remediation campaign** applies Pipefort's
[auto-fixes](/webapp/finding-triage) across every repository you select in one
action: the campaign scans each repo's workflow files, opens (or reuses) a fix
pull request per remediable file, and records the outcome per repository in a
tracker you can export as CSV.

## Launching a campaign

On the **Remediation** page:

1. Click **New campaign** in the toolbar.
2. Name it (e.g. *Pin actions to SHA — Q3*).
3. Select the repositories to include.
4. Click **Open PRs in N repos**.

The browser then drives the campaign the same way org-wide scanning works: one
short API request per repository, a few repos in flight at a time, so each
request stays well within serverless limits. You can watch the tracker fill in
live as repos complete.

<Note>
  **Admin only.** Launching a campaign — and remediating repos within one — is
  restricted to organization **admins**, since it opens pull requests across the
  org's repositories. Any member can view campaign trackers.
</Note>

## What gets fixed

Campaigns remediate **workflow-YAML findings that have an auto-fix** — the
same fixer registry behind the per-finding **Fix** button (pinning actions to
SHAs, adding least-privilege `permissions`, adding timeouts/concurrency,
removing debug logging, and so on). For each repository, the campaign:

* fetches and scans the repo's CI workflow files in memory (nothing is cloned),
* honors your [rule settings](/webapp/rule-settings) — disabled rules are
  never "fixed",
* opens one pull request per remediable workflow file — every fixable finding
  in that file is applied in a single commit, so a workflow with five issues
  gets one PR to review, not five — or reuses the existing Pipefort fix PR if
  one is already open,
* records the outcome.

Repo-settings findings (branch protection, secret scanning, …) are **not**
included — those are one-click direct fixes on the repository page, not pull
requests.

## Per-repo status

| Status        | Meaning                                                               |
| ------------- | --------------------------------------------------------------------- |
| **Pending**   | Not processed yet.                                                    |
| **Running**   | Remediation in flight.                                                |
| **PR opened** | At least one fix PR/MR was opened (or reused).                        |
| **Partial**   | Some files got fix PRs, but others failed — the row's error says why. |
| **No change** | Nothing auto-fixable — already remediated or no findings.             |
| **Failed**    | An error prevented remediation (see the row's error message).         |

The campaign itself is **running** until every repo reaches a terminal state,
then **completed**.

The queue opens on your most recent campaign; the picker in the toolbar
switches between them, and the tabs filter by outcome — **all**, **with PRs**,
**nothing to fix**, **failed** — each showing its own count. **CSV** downloads
the whole table: one row per repository with its status, PR count, fixes
applied, PR URLs, and any error.

<Warning>
  **Partial is not success.** A repository that opened a PR *and* reported an
  error got part of the way — the row is amber, not green, and names what was
  refused. Reading it as done leaves real findings unfixed.
</Warning>

## The fix tracker

Above the campaign queue, **Remediation** tracks every fix pull request
Pipefort has opened — from a campaign or from a single finding — in three
states:

| Tab                 | What is in it                                                                                                                                                                             |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Needs attention** | Something has stopped it: a merge conflict, a failing check, a review nobody has given, a branch-protection block — or it closed without merging, or it merged and the finding came back. |
| **In flight**       | Moving on its own: checks running, auto-merge armed, a fresh review request, still a draft, or ready to merge.                                                                            |
| **Landed**          | Merged. The row says whether a later scan has confirmed the finding gone.                                                                                                                 |

**Merged but the finding came back** is the row worth watching. Somebody
believed the problem was fixed and a later scan disagreed, which is a different
and more expensive failure than a fix nobody merged.

The header reports how many need a person, how many landed, and the **median
time from opening to merge**.

### Why a fix is stuck

What is happening inside an open pull request does not arrive on any webhook
Pipefort receives: mergeability is computed by GitHub only when asked, and
check conclusions and requested reviewers belong to event types the App does
not subscribe to. So it is **polled**, by the daily
`GET /api/cron/refresh-fix-prs`, and each row shows what GitHub said when it
was last asked.

| Row                                                                                           | What it means                                                                                                                     |
| --------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Merge conflict**                                                                            | The branch cannot merge into its base until conflicts are resolved by hand.                                                       |
| **Checks failed on the fix PR**                                                               | At least one check run concluded badly. The detail names it.                                                                      |
| **Blocked by branch protection**                                                              | Nothing is red and nothing is outstanding, yet the merge is refused — usually an approval this installation cannot give.          |
| **Awaiting review**                                                                           | A review has been requested and not given. It only moves to *needs attention* after 24 hours; below that, the process is working. |
| **Behind the base branch**                                                                    | The base moved on. Kept apart from a merge conflict because one is a button and the other is an afternoon.                        |
| **Still a draft** / **Checks running** / **Approved — auto-merge armed** / **Ready to merge** | Nothing is wrong. These sit in *in flight*.                                                                                       |

When several are true at once the row shows the **most blocking** one, because
the question it answers is "what is the next thing somebody has to do" — a pull
request with a conflict *and* a red check is a conflict, since resolving it
re-runs the checks.

<Note>
  A poll is a photograph. Every row is only true as of the moment it was taken,
  which is why the footnote states how recently. If the cron has never run, rows
  say **"opened, no outcome yet"** and the footnote says nothing has looked
  inside them — that is deliberately not the same as a clean bill of health.
</Note>

On a deployment with no [pull-request webhook](/webapp/pr-checks) configured,
no outcome is ever recorded: every fix stays **in flight**, and nothing is
called stale. Blaming a reviewer for a missing integration would be the wrong
sentence.

<Note>
  A merge is not a resolution. The finding stays open and stays in the posture
  counts until a later scan stops seeing it. A fix made by hand, outside Pipefort
  entirely, closes a finding the same way.
</Note>

## What the campaign queue cannot tell you

The queue below the tracker is about the moment the campaign ran — what it
managed to open, and what it could not. What became of each PR afterwards is
the tracker's job, not the queue's.

### Retrying

Launching a campaign never opens the same pull request twice: each repository is
claimed before its fixes run, so a double-click, a page refresh mid-run, or a
second admin working in another tab won't duplicate the work.

Repositories that **failed** can be retried — relaunching remediation for them
picks the campaign back up and returns it to **running**. Repositories that
already finished are left alone.

## GitLab

Campaigns are provider-neutral: for GitLab-hosted repositories the campaign
opens **merge requests** through the same flow.

## API

Campaigns are driven by three endpoints — see the
[API reference](/webapp/api-reference#post-apicampaigns):

* `POST /api/campaigns` — create a campaign over selected repos (admin only).
* `POST /api/campaigns/{id}/repos/{repo_id}/remediate` — remediate one repo
  (admin only; the SPA loops this with bounded concurrency).
* `GET /api/campaigns/{id}` — the tracker: campaign, per-repo statuses, and
  the rollup.

Campaign rows are also readable directly from the database
(`remediation_campaigns`, `campaign_repos`), scoped to your organizations.
