# Push policy

## Purpose

Git push policies are triggered on a per-stack basis to determine the action that should be taken for each individual [Stack](../stack) or [Module](../../vendors/terraform/module-registry) in response to a Git push or Pull Request notification. There are three possible outcomes:

* **track**: set the new head commit on the [stack](../stack) / [module](../../vendors/terraform/module-registry) and create a [tracked](../../run#where-do-runs-come-from) [Run](../run), ie. one that can be [applied](../../run#applying);
* **propose**: create a [proposed Run](../../run#where-do-runs-come-from) against a proposed version of infrastructure;
* **ignore**: do not schedule a new Run;

Using this policy it is possible to create a very sophisticated, custom-made setup. We can think of two main - and not mutually exclusive - use cases. The first one would be to ignore changes to certain paths - something you'd find useful both with classic monorepos and repositories containing multiple Terraform projects under different paths. The second one would be to only attempt to apply a subset of changes - for example, only commits tagged in a certain way.

### Git push policy and tracked branch

Each stack and module points at a particular Git branch called a [tracked branch](../../stack#repository-and-branch). By default, any push to the tracked branch triggers a tracked [Run](../run) that can be [applied](../../run#applying). This logic can be changed entirely by a Git push policy, but the tracked branch is always reported as part of the Stack input to the policy evaluator and can be used as a point of reference.



![The tracked branch head commit is behind the head commit of the stack.](https://3862190545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LshwwDZmA4HXN0k9e8O%2Fuploads%2F3LdHXxVck5DSkrC5qwcL%2FScreenshot%202022-07-05%20at%2014-48-52%20Runs%20%C2%B7%20example%20stack.png?alt=media\&token=ef23bd0d-f8e8-4034-a86c-58a202bd8a3a)

When a push policy does not track a new push, the head commit of the stack/module will not be set to the tracked branch head commit. We can address this by navigating to that stack and pressing the sync button (this syncs the tracked branch head commit with the head commit of the stack/module).

### Push and Pull Request events

Spacelift can currently react to two types of events - _push_ and _pull request_ (also called _merge request_ by GitLab). Push events are the default - even if you don't have a push policy set up, we will respond to those events. Pull request events are supported for some VCS providers and are generally received when you open, synchronize (push a new commit), label, or merge the pull request.

There are some valid reasons to use _pull request_ events in addition or indeed instead of push ones. One is that when making decisions based on the paths of affected files, push events are often confusing:

* they contain affected files for all commits in a push, not just the head commit;
* they are not context-aware, making it hard to work with pull requests - if a given push is ignored on an otherwise relevant PR, then the Spacelift status check is not provided;

But there are more reasons depending on how you want to structure your workflow. Here are a few samples of PR-driven policies from real-life use cases, each reflecting a slightly different way of doing things.

First, let's only trigger proposed runs if a PR exists, and allow any push to the tracked branch to trigger a tracked run:

```perl
package spacelift

track   { input.push.branch = input.stack.branch }
propose { not is_null(input.pull_request) }
ignore  { not track; not propose }
```

If you want to enforce that tracked runs are _always_ created from PR merges (and not from direct pushes to the tracked branch), you can tweak the above policy accordingly to just ignore all non-PR events:

```perl
package spacelift

track   { is_pr; input.push.branch = input.stack.branch }
propose { is_pr }
ignore  { not is_pr }
is_pr   { not is_null(input.pull_request) }
```

Here's another example where you respond to a particular PR label ("deploy") to automatically deploy changes:

```perl
package spacelift

track   { is_pr, labeled }
propose { true }
is_pr   { not is_null(input.pull_request) }
labeled { input.pull_request.labels[_] = "deploy" }
```

{% hint style="info" %}
When a run is triggered from a **GitHub** Pull Request and the Pull Request is **mergeable** (ie. there are no merge conflicts), we check out the code for something they call the "potential merge commit" - a virtual commit that represents the potential result of merging the Pull Request into its base branch. This should provide better quality, less confusing feedback.\
\
Let us know if you notice any irregularities.
{% endhint %}

#### Deduplicating events

If you're using pull requests in your flow, it is possible that we'll receive duplicate events. For example, if you push to a feature branch and then open a pull request, we first receive a _push_ event, then a separate _pull request (opened)_ event. When you push another commit to that feature branch, we again receive two events - _push_ and _pull request (synchronized)._ When you merge the pull request, we get two more - _push_ and _pull request_ _(closed)_.

It is possible that push policies resolve to the same actionable (not _ignore_) outcome (eg. _track_ or _propose_). In those cases instead of creating two separate runs, we debounce the events by deduplicating runs created by them on a per-stack basis.

The deduplication key consists of the commit SHA and run type. If your policy returns two different actionable outcomes for two different events associated with a given SHA, both runs will be created. In practice, this would be an unusual corner case and a good occasion to revisit your workflow.

When events are deduplicated and you're sampling policy evaluations, you may notice that there are two samples for the same SHA, each with different input. You can generally assume that it's the first one that creates a run.

### Canceling in-progress runs

The push policy can also be used to have the new run pre-empt any runs that are currently in progress. The input document includes the `in_progress` key, which contains an array of runs that are currently either still [queued](../../run#queued) or are [awaiting human confirmation](../../run/tracked#unconfirmed). You can use it in conjunction with the cancel rule like this:

```perl
cancel[run.id] { run := input.in_progress[_] }
```

Of course, you can use a more sophisticated approach and only choose to cancel a certain type of run, or runs in a particular state. For example, the rule below will only cancel proposed runs that are currently queued (waiting for the worker):

```perl
cancel[run.id] {
  run := input.in_progress[_]
  run.type == "PROPOSED"
  run.state == "QUEUED"
}
```

{% hint style="info" %}
Note that run preemption is _best effort_ and not guaranteed. If the run is either picked up by the worker or approved by a human in the meantime then the cancelation itself is canceled.
{% endhint %}

### Corner case: track, don't trigger

The `track` decision sets the new head commit on the affected stack or [module](../../vendors/terraform/module-registry). This head commit is what is going to be used when a tracked run is [manually triggered](../../run#where-do-runs-come-from), or a [task](../run/task) is started on the stack. Usually what you want in this case is to have a new tracked Run, so this is what we do by default.

Sometimes, however, you may want to trigger those tracked runs in a specific order or under specific circumstances - either manually or using a [trigger policy](trigger-policy). So what you want is an option to set the head commit, but not trigger a run. This is what the boolean `notrigger` rule can do for you. `notrigger` will only work in conjunction with `track` decision and will prevent the tracked run from being created.

Please note that `notrigger` does not depend in any way on the `track` rule - they're entirely independent. Only when interpreting the result of the policy, we will only look at `notrigger` if `track` evaluates to _true_. Here's an example of using the two rules together to always set the new commit on the stack, but not trigger a run - for example, because it's either always triggered [manually](../../run/tracked#triggering-manually), through [the API](../../integrations/api), or using a [trigger policy](trigger-policy):

```python
track     { input.push.branch == input.stack.branch }
propose   { not track }
notrigger { true }
```

### Take Action from Comment(s) on Pull Request(s)

Example policy to **trigger a tracked run from a pull request comment** event:

```
package spacelift

track {
    input.pull_request.action == "commented"
    input.pull_request.comment == concat(" ", ["/spacelift", "deploy", input.stack.id])
}
```

For more information on taking action from comments on Pull Requests, please view the documentation on pull request comments [here](../run/pull-request-comments).

### Customize Spacelift Ignore Event Behavior

**Customize VCS Check Messages for Ignored Run Events**

If you would like to customize messages sent back to your VCS when Spacelift runs are ignored, you can do so using the `message` function within your Push policy. Please see the example policy below as a reference for this functionality.

**Customize Check Status for Ignored Run Events**

By default, ignored runs on a stack will return a "skipped" status check event, rather than a fail event. If you would like ignored run events to have a failed status check on your VCS, you can do so using the `fail` function within your Push policy. If a `fail` result is desired, set this value to true.

**Example Policy**

The following Push policy does not trigger any run within Spacelift. Using this policy, we can ensure that the status check within our VCS (in this case, GitHub) fails and returns the message "I love bacon."

```
fail { true }
message["I love bacon"] ( true )
```

As a result of the above policy, users would then see this behavior within their GitHub status check:

![](https://3862190545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LshwwDZmA4HXN0k9e8O%2Fuploads%2F2zI4JJjL9fAUsv3wXtSf%2FScreen%20Shot%202022-06-13%20at%202.07.31%20PM.png?alt=media\&token=f75fd167-81c5-4947-bf15-46bb4b00bd8b)

{% hint style="info" %}
Note that this behavior (customization of the message and failing of the check within the VCS), **is only applicable when runs do not take place within Spacelift.**
{% endhint %}

### Tag-driven Terraform Module Release Flow

Some users prefer to manage their Terraform Module versions using git tags, and would like git tag events to push their module to the Spacelift module registry. Using a fairly simple Push policy, this is supported. To do this, you'll want to make sure of the `module_version` block within a Push policy attached your module, and then set the version using the tag information from the git push event.

**Example Policy**

For example, the following example Push policy will trigger a tracked run when a tag event is detected. The policy then parses the tag event data and uses that value for the module version (in the below example we remove a git tag prefixed with `v` as the Terraform Module Registry only supports versions in a numeric `X.X.X` format.

```
package spacelift

module_version := version {
    version := trim_prefix(input.push.tag, "v")
}

propose { true }
track { input.push.tag != "" }
```

### Allow forks

By default, we don't trigger runs when a forked repository opens a pull request against your repository. This is because of a security concern: if let's say your infrastructure is open source, someone forks it, implements some unwanted junk in there, then opens a pull request for the original repository, it'd run automatically with the prankster's code included.

{% hint style="info" %}
The cause is very similar to GitHub Actions where they don't expose repository secrets when forked repositories open pull requests.
{% endhint %}

If you still want to allow it, you can explicitly do it with `allow_fork` rule. For example, if you trust certain people or organizations:

```
propose { true }
allow_fork {
  validOwners := {"johnwayne", "microsoft"}
  validOwners[input.pull_request.head_owner]
}
```

In the above case, we'll allow a forked repository to run, **only** if the owner of the forked repository is either `johnwayne` or `microsoft`.

`head_owner` field means different things in different VCS providers:

#### GitHub

In GitHub, `head_owner` is the organization or the person owning the forked repository. It's typically in the URL: `https://github.com/<head_owner>/<forked_repository>`

#### GitLab

In GitLab, it is the group of the repository which is typically the URL of the repository: `https://gitlab.com/<head_owner>/<forked_repository>`

#### Azure DevOps

Azure DevOps is a special case because they don't provide us the friendly name of the `head_owner`. In this case, we need to refer to `head_owner` as the ID of the forked repository's project which is a UUID. One way to figure out this UUID is to open `https://dev.azure.com/<organization>/_apis/projects` website which lists all projects with their unique IDs. You don't need any special access to this API, you can just simply open it in your browser.

[Official documentation](https://docs.microsoft.com/en-us/rest/api/azure/devops/core/projects/list) of the API.

#### BitBucket Cloud

In BitBucket Cloud, `head_owner` means [workspace](https://support.atlassian.com/bitbucket-cloud/docs/what-is-a-workspace/). It's in the URL of the repository: `https://www.bitbucket.org/<workspace>/<forked_repository>`.

#### BitBucket Datacenter/Server

In BitBucket Datacenter/Server, it is the project key of the repository. The project key is not the display name of the project, but the abbreviation in all caps.

![BitBucket Datacenter/Server project key](https://3862190545-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-LshwwDZmA4HXN0k9e8O%2Fuploads%2F5u9E2BqfeRS0X30n9V3D%2Fimage.png?alt=media\&token=1d0e6712-58ef-433f-bcce-85c6d7fa824a)

## Data input

As input, Git push policy receives the following document:

```javascript
{
  "pull_request": {
    "action": "string - opened, reopened, closed, merged, edited, labeled, synchronize, unlabeled",
    "base": {
      "affected_files": ["string"],
      "author": "string",
      "branch": "string",
      "created_at": "number (timestamp in nanoseconds)",
      "message": "string",
      "tag": "string"
    },
    "diff": ["string - list of files changed between base and head commit"],
    "head": {
      "affected_files": ["string"],
      "author": "string",
      "branch": "string",
      "created_at": "number (timestamp in nanoseconds)",
      "message": "string",
      "tag": "string"
    },
    "head_owner": "string",
    "labels": ["string"],
    "title": "string"
  }
  "push": {
    // For Git push events, this contains the pushed commit.
    // For Pull Request events,
    // this contains the head commit or merge commit if available (merge event).
    "affected_files": ["string"],
    "author": "string",
    "branch": "string",
    "created_at": "number (timestamp in nanoseconds)",
    "message": "string",
    "tag": "string"
  },
  "stack": {
    "administrative": "boolean",
    "branch": "string",
    "labels": ["string - list of arbitrary, user-defined selectors"],
    "locked_by": "optional string - if the stack is locked, this is the name of the user who did it",
    "name": "string",
    "namespace": "string - repository namespace, only relevant to GitLab repositories",
    "project_root": "optional string - project root as set on the Stack, if any",
    "repository": "string",
    "state": "string",
    "terraform_version": "string or null"
  },
  "in_progress": [{
    "based_on_local_workspace": "boolean - whether the run stems from a local preview",
    "changes": [
      {
        "action": "string enum - added | changed | deleted",
        "entity": {
          "address": "string - full address of the entity",
          "name": "string - name of the entity",
          "type": "string - full resource type or \"output\" for outputs"
        },
        "phase": "string enum - plan | apply"
      }
    ],
    "created_at": "number - creation Unix timestamp in nanoseconds",
    "runtime_config": {
      "before_init": ["string - command to run before run initialization"],
      "project_root": "string - root of the Terraform project",
      "runner_image": "string - Docker image used to execute the run",
      "terraform_version": "string - Terraform version used to for the run"
    },
    "triggered_by": "string or null - user or trigger policy who triggered the run, if applicable",
    "type": "string - PROPOSED or TRACKED",
    "updated_at": "number - last update Unix timestamp in nanoseconds",
    "user_provided_metadata": ["string - blobs of metadata provided using spacectl or the API when interacting with this run"]
  }]
}
```

Based on this input, the policy may define boolean `track`, `propose` and `ignore` rules. The positive outcome of at least one `ignore` rule causes the push to be ignored, no matter the outcome of other rules. The positive outcome of at least one `track` rule triggers a _tracked_ run. The positive outcome of at least one `propose` rule triggers a _proposed_ run.

{% hint style="warning" %}
If no rules are matched, the default is to **ignore** the push. Therefore it is important to always supply an exhaustive set of policies - that is, making sure that they define what to **track** and what to **propose** in addition to defining what they **ignore**.
{% endhint %}

It is also possible to define an auxiliary rule called `ignore_track`, which overrides a positive outcome of the `track` rule but does not affect other rules, most notably the `propose` one. This can be used to turn some of the pushes that would otherwise be applied into test runs.

## Use cases

### Ignoring certain paths

Ignoring changes to certain paths is something you'd find useful both with classic monorepos and repositories containing multiple Terraform projects under different paths. When evaluating a push, we determine the list of affected files by looking at all the files touched by any of the commits in a given push.

{% hint style="info" %}
This list may include false positives - eg. in a situation where you delete a given file in one commit, then bring it back in another commit, and then push multiple commits at once. This is a safer default than trying to figure out the exact scope of each push.
{% endhint %}

Let's imagine a situation where you only want to look at changes to Terraform definitions - in HCL or [JSON](https://www.terraform.io/docs/configuration/syntax-json.html)  - inside one the `production/` or `modules/` directory, and have track and propose use their default settings:

```perl
package spacelift

track   { input.push.branch == input.stack.branch }
propose { input.push.branch != "" }
ignore  { not affected }

affected {
	some i, j, k

  tracked_directories := {"modules/", "production/"}
  tracked_extensions := {".tf", ".tf.json"}

	path := input.push.affected_files[i]

  startswith(path, tracked_directories[j])
  endswith(path, tracked_extensions[k])
}
```

As an aside, note that in order to keep the example readable we had to define `ignore` in a negative way as per [the Anna Karenina principle](https://en.wikipedia.org/wiki/Anna_Karenina_principle). A minimal example of this policy is available [here](https://play.openpolicyagent.org/p/2jjy1kSGBM).

### Status checks and ignored pushes

By default when the push policy instructs Spacelift to ignore a certain change, no commit status check is sent back to the VCS. This behavior is explicitly designed to prevent noise in monorepo scenarios where a large number of stacks are linked to the same Git repo.

However, in certain cases one may still be interested in learning that the push was ignored, or just getting a commit status check for a given stack when it's set as required as part of [GitHub branch protection](https://docs.github.com/en/github/administering-a-repository/managing-a-branch-protection-rule) set of rules, or simply your internal organization rules.

In that case, you may find the `notify` rule useful. The purpose of this rule is to override default notification settings. So if you want to notify your VCS vendor even when a commit is ignored, you can define it like this:

```perl
package spacelift

# other rules (including ignore), see above

notify { ignore } 
```

{% hint style="info" %}
The notify rule (_false_ by default) only applies to ignored pushes, so you can't set it to `false` to silence commit status checks for [proposed runs](../run/proposed).
{% endhint %}

## Applying from a tag

Another possible use case of a Git push policy would be to apply from a newly created tag rather than from a branch. This in turn can be useful in multiple scenarios - for example, a staging/QA environment could be deployed every time a certain tag type is applied to a tested branch, thereby providing inline feedback on a GitHub Pull Request from the actual deployment rather than a plan/test. One could also constrain production to only apply from tags unless a Run is explicitly triggered by the user.

Here's an example of one such policy:

```perl
package spacelift

track   { re_match(`^\d+\.\d+\.\d+$`, input.push.tag) }
propose { input.push.branch != input.stack.branch }
```

## Default Git push policy

If no Git push policies are attached to a stack or a module, the default behavior is equivalent to this policy:

```perl
package spacelift

track   { input.push.branch == input.stack.branch }
propose { input.push.branch != "" }
ignore  { input.push.branch == "" }
```
