# Repository policies

Set up .mirage/config.yml, review policy changes, and test scoped previews.

Repository policies control repository-wide Mirage behavior through reviewed configuration.
The active file lives in the repository, so policy changes use the same pull requests,
approvals, branch protection, and audit history as the code they affect.

## Set up a repository policy

The recommended setup takes place in Mirage:

1. Select the private repository for the Mirage GitHub App.
2. Open [Repos](https://mirageci.com/repos), select the repository, and find its **Config** section.
3. Turn on the behavior you want. Mirage shows the active value beside the proposed value.
4. Select **Open draft PR**. Mirage validates the policy, creates `.mirage/config.yml`, and
   opens a draft pull request against the default branch.
5. Review the generated file, let the repository's normal checks run, and merge the pull
   request when it is ready.
6. Return to the repository in Repos. The policy is active when Mirage reports that it comes
   from the repository's default branch.

Mirage creates the `.mirage` directory and the config file if they do not exist. Opening a
draft does not activate anything; the change becomes active only after it reaches the default
branch.

> **Warning: The exact filename matters.**
>
> Mirage reads `.mirage/config.yml`, including the leading dot and the `.yml` extension. It does
> not read `mirage/config.yaml`, `.mirage/config.yaml`, or a config file stored only on a feature
> branch.

## Understand the source of truth

The copy of `.mirage/config.yml` on the repository's default branch is the permanent source
of truth. A repository's Config section in Repos is an editor for that file, not a separate
configuration store.
You can use either workflow:

- **Repos:** Mirage validates the form, shows a plan, and opens or updates a draft pull
  request.
- **Code:** edit `.mirage/config.yml` directly, commit it on a branch, and open a pull request
  yourself.

Mirage resolves policy in this order:

| Priority | Source | When it applies |
| --- | --- | --- |
| 1 | Scoped preview | The selected PR, target commit, proposal commit, and expiry all match |
| 2 | Default branch | `.mirage/config.yml` exists and passes the versioned schema |
| 3 | Safe defaults | The file is absent or invalid; all new policy behavior stays off |

Mirage stores draft-proposal and preview bookkeeping in its database, but that operational
state never replaces the default-branch file. Merging a policy PR changes behavior without a
Mirage deployment.

## Create the file manually

From the repository root, create the policy directory:

Create the policy directory:

```sh
mkdir -p .mirage
```

Then add the complete version 1 policy:

.mirage/config.yml:

```yaml
# Repository-wide Mirage behavior. Keep this file on the default branch.
version: 1
pull_requests:
  cancel_workflow_runs:
    on_merge: true
    on_close: false
```

Commit the file on a branch and merge it through the repository's normal review process. You
do not need to restart a runner or redeploy Mirage. To remove the behavior, change the values
to `false` or remove the file in another reviewed pull request.

YAML comments are allowed. Do not put credentials or tokens in this file; it is ordinary
repository content and may be visible to everyone who can read the repository.

## Configuration reference

Mirage uses a strict, versioned schema. `version` is required and must currently be `1`.
Optional sections receive safe defaults, but misspelled and unknown keys are rejected so a
policy cannot appear enabled while doing nothing.

| Key | Type | Default | Effect |
| --- | --- | --- | --- |
| `version` | `1` | Required | Selects the repository-policy schema |
| `pull_requests` | Mapping | All PR behavior off | Groups behavior triggered by PR lifecycle events |
| `pull_requests.cancel_workflow_runs` | Mapping | Both values `false` | Controls whole-run cancellation |
| `pull_requests.cancel_workflow_runs.on_merge` | Boolean | `false` | Cancels unfinished PR workflow runs after a merge |
| `pull_requests.cancel_workflow_runs.on_close` | Boolean | `false` | Cancels unfinished PR workflow runs after an unmerged close |

The smallest valid file keeps every optional behavior disabled:

Minimal safe policy:

```yaml
version: 1
```

Merge and unmerged-close behavior are independent. For example, this keeps CI running when a
PR merges but cancels it when someone closes the PR without merging:

Cancel only after an unmerged close:

```yaml
version: 1
pull_requests:
  cancel_workflow_runs:
    on_merge: false
    on_close: true
```

Use YAML booleans (`true` and `false`), not quoted strings such as `"true"`. Future schema
changes will use a new version instead of silently changing version 1 semantics.

## Review changes from Repos

Changing a switch shows an active-versus-proposed plan. Select **Open draft PR** to validate
the complete policy, write it to a generated branch, and open a draft pull request. Further
edits update that same draft while it remains open and its branch has not been changed outside
Mirage.

The generated PR is intentionally ordinary GitHub configuration:

- repository checks and required approvals still apply;
- reviewers can inspect the exact YAML before it becomes active;
- closing the draft leaves active behavior unchanged; and
- merging it makes the default-branch file authoritative.

If someone edits or force-pushes the generated branch outside Mirage, Mirage stops updating
that proposal rather than overwriting their work. Close it and create a new proposal, or
finish the change directly in GitHub.

## Test one exact revision

After opening the policy draft, enter the number of another open pull request and select
**Start 2-hour preview**. A preview:

- applies only to the selected pull request;
- requires that pull request to target the policy proposal's base branch;
- pins both the selected pull request head and the exact policy proposal commit;
- expires after two hours; and
- becomes invalid immediately if either revision changes or the policy proposal closes.

Preview state is operational data held by Mirage. It never rewrites the target PR and never
replaces the policy on the default branch. For cancellation policies, merging or closing the
selected test PR is the lifecycle event that exercises the preview.

Because cancellation changes a live GitHub Actions run, use a disposable test PR when you
first evaluate the behavior.

## Know what cancellation affects

The two lifecycle settings are mutually exclusive for a single PR event:

| GitHub event | Setting consulted |
| --- | --- |
| PR merged | `on_merge` |
| PR closed without merging | `on_close` |

When the selected setting is enabled, Mirage finds unfinished workflow runs that GitHub
associated with that pull request and asks GitHub Actions to cancel each whole run. This
includes workflows reported against GitHub's synthetic merge commit.

Cancellation is deliberately narrow:

- it cancels an entire workflow run, not an individual Mirage runner job;
- queued and in-progress PR runs are eligible;
- completed runs are left unchanged;
- `push`, `workflow_dispatch`, and other non-PR runs are not selected; and
- runs associated with another pull request are not selected, even if a commit SHA overlaps.

This policy covers jobs that have not reached a Mirage runner as well as jobs routed to other
providers. GitHub remains responsible for transitioning the run to its final cancelled state.

Workflow concurrency groups do not do this for you. A group keyed on `github.ref`, such as
the common `${{ github.workflow }}-${{ github.ref }}`, puts a pull request’s runs
(`refs/pull/<number>/merge`) in a different group from the push run that the merge starts on
the default branch. The merge therefore does not cancel the pull request’s unfinished run
through concurrency; `on_merge` does.

## Protect and authorize policy changes

Creating proposals requires repository-management access in Mirage and the following GitHub
App repository permissions:

- **Contents: write** to create the config branch and file;
- **Pull requests: write** to open a draft PR; and
- **Actions: write** to cancel a matching workflow run.

If an existing installation predates one of these permissions, GitHub may ask an organization
owner to approve the updated App permissions. See [Connect GitHub](https://mirageci.com/docs/connect-github.md#permissions)
for installation and access management.

Treat the file like other sensitive CI configuration. Require pull-request review, protect the
default branch, and optionally assign a code owner:

CODEOWNERS:

```text
.mirage/config.yml @your-org/ci-admins
```

## Troubleshoot a policy

| Symptom | What to check |
| --- | --- |
| Repos says the policy is missing | Confirm the exact path is `.mirage/config.yml` on the repository's default branch |
| Repos says the policy is invalid | Check `version: 1`, YAML indentation, Boolean values, and unknown or misspelled keys |
| Mirage cannot open the draft PR | Approve the App's Contents, Pull requests, and Actions permissions; confirm the repository is active and not archived |
| A merged draft is not active | Confirm the PR merged into the default branch and that its final file is valid |
| A preview disappeared | Previews expire after two hours and are invalidated by a force-push or a changed/closed policy proposal |
| A run was not cancelled | Confirm the matching merge/close setting is `true`, the run came from `pull_request`, and it was still unfinished when the lifecycle event arrived |
| A push run kept running | Expected: cancellation policies select PR-associated runs only |

When the default-branch file is absent or cannot be parsed, Mirage fails safe: it does not
introduce cancellation behavior. Correct or revert the file through the repository's normal
review process, then reload the repository's page in Repos.
