Repository: prometheus/alertmanager
Pull request #3590 (2023-11-01 20:00:08)

jira integration
This PR track the Atlassian JIRA integration into Alertmanger.

See: #3577 

Replaces: https://github.com/prometheus-community/jiralert

---

What I test:

* Jira Cloud only
* Create issues
* Auto-Close issue with `resolve_transition`
* Reopen issue with `reopen_transition`
* Reopen duration
* Custom Fields
* `api_token_file`

k0ste: Can this integration cover another of jiralert issue - is custom field for alert hash?
https://github.com/prometheus-community/jiralert/issues/164

Current code search hash in hardcoded place - in `labels`

```go
jql.WriteString(fmt.Sprintf(`project = "%s" and
labels=%q order by status ASC,
resolutiondate DESC`, n.conf.Project, key.Hash()))
```

Thanks
jkroepke: Sure. After merge. Everyone is invited to improve.

I would not like to add more feature here. Even I understand you request.

The current PR covery already a lot of cases to test. Adding more new features results into more testing. I would prefer to add new features in separate PRs.
holger-waschke: Is this implementation only for [Jira Cloud ](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#about) or can I still use the on-premise API with a PAT Authentication?
jkroepke: Hi @holger-waschke 

I'm not able to test on-premise. But you can enter an Server API Path to an on premise instance and the authentication is still auth basic which should work on on-premise, too.

The integration does not use any cloud specific features. If https://github.com/prometheus-community/jiralert works on-premise, this integration should work, too.

I remember that Issue handling, the Jira Software Server API v2 and Cloud API v2 is the same.

* https://docs.atlassian.com/software/jira/docs/api/REST/9.11.0/#api/2/issue
* https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issues/#api-rest-api-2-issue-post

But please keep in mind that this integration will always have best-effort since most users are not able to setup a on premise instance. 
jkroepke: > Thanks for starting this! I haven't looked yet in details into the notifier but I suppose that it's almost a 1:1 copy from the original project?

No. while jiralert uses a unofficial outdated REST SDK for JIRA, I used the standard http client to setup the few http calls.
jkroepke: @simonpasquier I tested the PR after review and code changes again.
jkroepke: @simonpasquier could you please take a look here? Thanks you.
christianjedroCDT: i´ve tested the PR with our Jira Cloud Setup and everything looks good so far. Would be really interested in using this, is there an update @simonpasquier? 
Matz13: Showing some additional support here! The tests I ran all look good and I can't wait to get the official release.
jkroepke: I fixed all the pipeline issues now
jkroepke: @simonpasquier thanks for taking an other look here.

Since I open the PR 7 months ago, I learned bit more about go. In conclusion, I added some units tests and I was able to identity some bugs.

Sadly, adding the units tests here requires additional review cycles.

I will continue some integrations tests and once I'm done, I will re-request an review.
jkroepke: I have tested the priority template together with `inhibit_rules` which works really great together.

```yaml
inhibit_rules:
  - equal:
      - namespace
      - alertname
    source_matchers:
      - severity = critical
    target_matchers:
      - severity =~ warning|info
  - equal:
      - namespace
      - alertname
    source_matchers:
      - severity = warning
    target_matchers:
      - severity = info
```

Manual testing against Jira Atlassian Cloud works fine.

In conclusion, we have 2 open topic:

- `fields` vs `custom_fields` inside jira_config
- `github.com/trivago/tgo` for cast nested map from `map[any]any` to `map[string]any`
jkroepke: > The code looks good to me. Unfortunately, I cannot test in my setup, because I have a problem with the field Labels. I'll way the code be merged to include https://github.com/prometheus-community/jiralert/

I‘m open to extend the feature set after this PR got merged.

Background: it seems like AM has some review capacity issues and increase the scope involves an additional review each time. The current scope is well tested (including manual test against prod systems).
SuperQ: Yes, I would like to see this merged so we can iterate on additional featres.

Patch:
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index 8de7af63..83ae3906 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -24,7 +24,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Checkout repository
-        uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
+        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
       - name: Install Go
         uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
         with:
@@ -36,4 +36,4 @@ jobs:
         uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1
         with:
           args: --verbose
-          version: v1.59.0
+          version: v1.59.1
diff --git a/Makefile.common b/Makefile.common
index 16172923..e3da72ab 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -61,7 +61,7 @@ PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_
 SKIP_GOLANGCI_LINT :=
 GOLANGCI_LINT :=
 GOLANGCI_LINT_OPTS ?=
-GOLANGCI_LINT_VERSION ?= v1.59.0
+GOLANGCI_LINT_VERSION ?= v1.59.1
 # golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
 # windows isn't included here because of the path separator being different.
 ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
diff --git a/asset/assets_vfsdata.go b/asset/assets_vfsdata.go
index f74112ad..16ab608e 100644
--- a/asset/assets_vfsdata.go
+++ b/asset/assets_vfsdata.go
@@ -163,9 +163,9 @@ var Assets = func() http.FileSystem {
 		"/templates/default.tmpl": &vfsgen۰CompressedFileInfo{
 			name:             "default.tmpl",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 5951,
+			uncompressedSize: 7283,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x58\x4f\x6f\xbb\x46\x10\xbd\xf3\x29\x56\xce\x25\x3e\x98\xf4\x1c\x29\xaa\xa2\xaa\xed\x25\xaa\x2a\x47\xee\xa5\xaa\xd0\x1a\xc6\x64\xe3\xfd\x43\x76\x07\x27\x16\xe6\xbb\x57\x0b\xc4\x06\x2f\x38\x8b\xe3\xdf\xe9\xe7\x5b\xd8\xcc\xbc\x99\x7d\x6f\x98\x19\x5c\x14\x24\x81\x15\x93\x40\x26\x51\x44\x39\x68\x14\x54\xd2\x14\xf4\x84\x94\xe5\x63\xeb\xb9\x28\x08\xc8\x84\x94\x65\x30\xe8\xb2\x98\x3f\x59\xaf\xa2\x20\xe1\xef\x1f\x08\x5a\x52\xbe\x98\x3f\x91\xb2\xbc\xbb\xb9\xab\xec\xcc\xaf\x1a\x62\x60\x1b\xd0\x0f\xd6\x68\xde\x3c\x90\x1d\xc9\x35\x7f\xcb\x41\x6f\x6b\xf7\x26\x50\x37\x92\xc9\x97\xaf\x10\xa3\x8d\xf0\xaf\xf5\x7e\x46\x8a\xb9\x21\x3b\x82\x6a\x91\x65\xa0\x6b\x57\xb6\x22\xf0\xb6\xff\xe7\x64\xc5\x34\x93\xa9\xf5\xb9\xb7\x3e\xd5\x85\x4c\xf8\x47\x75\x4a\x76\x84\x83\x6c\x47\xfc\x8f\x58\xa3\x3f\xb5\xca\xb3\x27\xba\x04\x6e\xc2\x67\xa5\x11\x92\xbf\x29\xd3\x26\xfc\x87\xf2\x1c\x6c\xc0\x57\xc5\x24\x99\x10\x8b\x4a\xea\x90\x29\x92\x5b\x8b\x15\xfe\xa6\x84\x50\xb2\x76\x9e\x36\x67\x2d\xbc\x29\x29\xcb\xdb\xa2\x20\xef\x0c\x5f\xba\xc6\xe1\x1c\x84\xda\x40\x37\xfa\x5f\x54\x80\x69\x18\xed\x8b\xbe\x4f\x7c\xba\xff\x6b\x40\xa6\x04\x4c\xac\x59\x86\x4c\xc9\xc9\x09\x8e\x11\x3e\xb0\x96\x34\xe2\xcc\x60\x63\xaa\xa9\x4c\x81\x84\xa4\x2c\xeb\xbc\xee\x83\xc3\xa1\xcb\x93\x65\x65\x56\x11\x69\xd3\xb7\x4f\x0f\x64\x7f\x81\x26\xb1\x3a\xf8\xa3\x94\x0a\xa9\xcd\xa9\x03\xd9\x3a\x3e\x0f\xf7\x59\xe5\x3a\x86\xfb\x5a\x4c\x90\xa0\x29\x2a\x5d\x57\x62\xd0\x43\xd4\x49\x0a\x22\x41\xf5\x3a\x51\xef\xd2\xe1\x22\xf0\x25\xc3\x33\xeb\x60\x3c\x1d\xbe\xc8\x5e\x84\x04\xfd\x8c\x18\x4e\xe3\x75\x98\xc0\x8a\xe6\x1c\x43\x64\xc8\xa1\xa1\x02\x41\x64\x9c\x62\xf7\xe5\x0c\x87\x6a\xb0\x8b\x93\x1b\xdb\x1e\x44\x1f\x54\xb7\x09\x79\xe2\xad\x28\xe7\x4b\x1a\xaf\x1d\xbc\xde\xf4\x2d\x28\xd9\x91\xaf\x0c\x39\x93\x6b\xef\x0c\xe2\x26\x03\x96\x4c\xfc\x1c\x32\x0d\xb6\xd6\x3c\xad\x5b\x09\x9d\x64\xac\xea\xc1\x9e\x29\xb3\x58\x49\x10\xea\x95\x4d\xfc\xed\x73\xcd\x7d\x33\xf6\xbf\xdc\x4a\x29\xac\x27\x4e\xab\x06\xdb\xe6\x99\xbd\x5a\x92\xe3\x76\xef\xe2\x36\xb4\x71\xe5\xe8\x22\xc6\x9c\x81\xc4\xf3\x0b\x72\x08\xf1\x30\x15\xcf\xd3\xcc\xc5\x65\xd2\x20\x95\x31\x98\x1e\x5c\xa7\x83\x87\xc3\xac\xaa\xcc\xa4\x20\x19\xec\x81\x05\x18\x43\xd3\xf3\xde\x6f\x07\xcc\x55\xa8\x19\x78\x03\x0d\xad\x77\xc2\x05\x47\xf3\xb5\x33\xc0\xa7\xe4\x17\x32\xb3\x8d\xb3\x3a\x24\xf5\x61\xd5\x3a\x4f\x33\xd2\xdd\x02\xaa\x20\xb3\xd6\x8d\x7a\xe2\xcd\xc1\x28\xbe\x81\xe4\x28\xe2\xe7\xb1\x7f\xcc\x4f\x0f\x27\xea\xcc\x87\x52\x53\xf5\xf1\xf1\xd5\xd4\x51\xfd\x1d\xe2\x17\x8a\x63\x35\x0f\xae\xfa\x9d\xd0\xaf\xbd\x28\x2f\x34\x77\xf0\x7a\xf5\x19\x50\xfd\x48\x1f\x54\x91\x1d\x96\x83\x9d\xd4\x35\xcf\xa8\xc6\xed\x08\x7b\xa4\xa9\xaf\x35\x4d\x41\x62\x74\x3c\xe2\xba\xf5\xb5\x61\x31\x2a\xad\x32\x73\x28\x5b\xa4\x08\x51\xb7\xd0\xae\xb5\x34\xae\x17\xb8\xac\x82\x44\x86\xdb\x28\x61\x26\xe3\x74\x1b\x0d\x6c\x53\x5f\x37\x6e\x17\x59\x28\xc9\x50\x59\x42\x22\x54\x8a\x8f\x1c\x89\x9d\xd9\x95\x9b\x17\xb5\x01\x7d\x81\xfd\xd1\x81\xfa\xf1\xf5\x74\x99\x72\xf2\xaf\xa6\xcb\x15\x93\xbb\xd2\x9f\x62\xf2\xb0\xd3\x8d\x99\x29\xed\x6d\x4e\xb6\x5e\xf6\xc3\x67\xfa\xf8\x6f\x84\x16\xce\x55\xde\x31\xf2\xb6\x59\x44\xe0\x90\x6a\x2a\xfa\xa8\xfc\x69\x49\x49\x98\x89\x95\x4e\x2e\xd0\x88\x8e\x91\xae\xec\xda\x35\x61\x09\x1f\xd7\x57\xf7\xdb\x3c\x0a\x83\x40\x45\xbb\x99\x0a\x41\xf5\xf6\xac\x3a\x3d\xc6\x3a\xbf\xe2\x1d\xa4\xe6\xcb\xde\x47\xa6\x1b\x32\x4a\xa8\xd6\xcf\x6d\xdf\x56\x6c\x1f\xda\x57\xb3\x9e\xe0\x5f\x89\xf7\x7f\x00\x00\x00\xff\xff\xf6\x0e\x88\xb1\x3f\x17\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x59\xcd\x6e\xeb\x36\x13\xdd\xfb\x29\x06\xba\xdf\x22\x5e\x44\xf7\x5b\x07\x08\x8a\x8b\xa2\x3f\x8b\xb4\x28\x1c\xa4\x9b\xa2\x10\x18\x69\xac\x30\xa1\x48\x85\x1c\xd9\x31\x1c\xbd\x7b\x41\x49\x96\x45\x49\xb6\x29\x5f\x77\x55\xef\x62\x7a\xe6\x9c\xe1\x39\xa3\x21\xe5\x6c\xb7\x90\xe0\x92\x4b\x84\x20\x8a\x98\x40\x4d\x19\x93\x2c\x45\x1d\x40\x59\x7e\xeb\x7c\xde\x6e\x01\x65\x02\x65\x39\x3b\x98\xf2\xb4\x78\xb0\x59\xdb\x2d\x84\x3f\x7d\x10\x6a\xc9\xc4\xd3\xe2\x01\xca\xf2\xeb\x97\xaf\x55\x9c\xf9\x41\x63\x8c\x7c\x85\xfa\xde\x06\x2d\x9a\x0f\xf0\x09\x85\x16\xef\x05\xea\x4d\x9d\xde\x10\xb9\x4c\xa6\x78\x7e\xc5\x98\x2c\xc3\x5f\x36\xfb\x91\x18\x15\x06\x3e\x81\xd4\x53\x9e\xa3\xae\x53\xf9\x12\xf0\xbd\xfd\x32\x58\x72\xcd\x65\x6a\x73\xee\x6c\x4e\xb5\x21\x13\xfe\x5c\xad\xc2\x27\x08\x94\x5d\xc6\xbf\xc1\x06\xfd\xa2\x55\x91\x3f\xb0\x67\x14\x26\x7c\x54\x9a\x30\xf9\x83\x71\x6d\xc2\x3f\x99\x28\xd0\x12\xbe\x2a\x2e\x21\x00\x8b\x0a\x35\x65\x4a\x70\x63\xb1\xc2\x1f\x55\x96\x29\x59\x27\xcf\x9b\xb5\x0e\xde\x1c\xca\xf2\x66\xbb\x85\x35\xa7\x17\x37\x38\x5c\x60\xa6\x56\xe8\xb2\xff\xce\x32\x34\x8d\xa2\x63\xec\x6d\xe1\xf3\xf6\xaf\x03\x36\x25\x68\x62\xcd\x73\xe2\x4a\x06\x47\x34\x26\xfc\xa0\xda\xd2\x48\x70\x43\x4d\xa8\x66\x32\x45\x08\xa1\x2c\xeb\xba\xee\x66\xfb\xc5\xa1\x4e\x56\x95\xdb\x4a\x48\x5b\xbe\xfd\x74\x0f\xed\x06\x9a\xc2\x6a\xf2\x6f\x52\x2a\x62\xb6\x26\x07\xb2\xb3\x7c\x1e\xee\xa3\x2a\x74\x8c\x77\xb5\x99\x28\x51\x33\x52\xba\xee\xc4\xd9\x88\x50\x47\x25\x88\x32\xa6\xdf\x12\xb5\x96\x03\x2d\x66\xbe\x62\x78\x56\x3d\x9b\x2e\x87\x2f\xb2\x97\x20\xb3\x71\x45\x8c\x60\xf1\x5b\x98\xe0\x92\x15\x82\x42\xe2\x24\xb0\x91\x82\x30\xcb\x05\x23\xf7\xe1\x0c\x0f\xf5\xa0\x8b\x53\x18\x3b\x1e\xb2\x31\x28\x77\x08\x79\xe2\x2d\x99\x10\xcf\x2c\x7e\x1b\xe0\x8d\x96\x6f\x41\xe1\x13\x4e\x05\x0a\x2e\xdf\xbc\x2b\x88\x9b\x0a\x78\x12\xf8\x25\xe4\x1a\x6d\xaf\x79\x46\x77\x0a\x3a\xaa\x58\x35\x83\x3d\x4b\xe6\xb1\x92\x98\xa9\x57\x1e\xf8\xc7\x17\x5a\xf8\x56\xec\xbf\xb9\xa5\x52\x54\x9f\x38\x9d\x1e\xec\x86\xe7\x76\x6b\x49\x41\x9b\x36\x65\x38\xd0\xa6\xb5\xe3\x10\x31\x16\x1c\x25\x9d\xdf\x90\x87\x10\xf7\xa7\xe2\x79\x9e\x0d\x71\xb9\x34\xc4\x64\x8c\x66\x04\x77\x30\xc1\xc3\xc3\xaa\xaa\xdc\xa4\x28\x39\xb6\xc0\x19\x1a\xc3\xd2\xf3\x9e\xef\x01\xd8\xd0\xa1\xe6\xc0\x3b\x30\xd0\x46\x4f\xb8\x59\xef\x7c\x75\x0e\xf0\x39\xfc\x1f\x6e\xed\xe0\xac\x16\xa1\x5e\xac\x46\xe7\x71\x45\xdc\x5b\x40\x45\x72\xdb\xd9\xd1\x08\xdf\x02\x8d\x12\x2b\x4c\x7a\x8c\xbb\x65\x7f\xce\x5d\xc6\x80\xf5\xd6\x47\x52\x53\xcd\xf1\xe9\xdd\xe4\xb8\xbe\xc6\xf8\x85\xd1\x54\xcf\x67\x57\xff\x8e\xf8\xd7\xbd\x28\x3f\x69\x31\xc0\x1b\xf5\xe7\x80\xeb\x3d\x7f\x48\x45\xf6\xb0\x3c\x38\x49\x87\xe1\x39\xd3\xb4\x99\x10\x4f\x2c\xf5\x8d\x66\x29\x4a\x8a\xfa\x47\x9c\xdb\x5f\x2b\x1e\x93\xd2\x2a\x37\xfb\xb6\x25\x46\x18\xb9\x8d\x76\xed\xa5\x69\xb3\x60\xa8\x2a\x4a\xe2\xb4\x89\x12\x6e\x72\xc1\x36\xd1\x81\xdb\xd4\xe9\xc1\x3d\x44\xce\x94\xe4\xa4\xac\x20\x11\x29\x25\x26\x1e\x89\xce\xd9\x55\x98\x17\xb5\x42\x7d\x81\xfb\xe3\x00\xea\xdf\xef\xa7\xcb\xb4\x93\x7f\x37\x5d\xae\x99\x86\x57\xfa\x63\x4a\xee\xef\x74\x53\xce\x94\xee\x6d\x4e\x76\x1e\xf6\xfd\x6b\xfa\xf4\x77\x84\x0e\xce\xd5\xde\x29\xf6\x76\x55\x24\x14\x98\x6a\x96\x8d\x49\xf9\x9f\x15\x25\xe1\x26\x56\x3a\xb9\xc0\x20\xea\x23\x5d\xd5\xb5\xd7\x84\x67\xfc\xb8\x3e\xba\xdf\xad\x63\x66\x08\x59\xd6\x1d\xa6\x59\xc6\xf4\xe6\xac\x3e\xed\x63\x9d\xdf\xf1\x03\xa4\xe6\xcd\xde\xc7\xa6\x2f\x30\xc9\xa8\xce\xcf\x6d\xdf\xed\x58\x4b\xed\xeb\xd9\x08\xf9\x14\xf3\x5e\xb9\x66\x17\x71\xce\x01\xea\xbd\x46\x5f\x35\x9f\x55\x37\xe4\x51\xad\x72\xcd\x95\xe6\xf6\xe5\xe7\xb6\xb9\x48\xff\x6f\xb7\x04\x77\xf7\x10\x04\xbb\xfb\xf5\xee\xa7\x55\x67\xb7\x36\x07\x00\xa0\xca\x33\xb8\xc2\x5d\x1e\x97\x09\x7e\xec\x7e\xdd\x85\x60\xf7\x55\xe0\x64\xf0\x25\xdc\xe0\x7b\x27\x31\x88\x35\x27\x1e\x33\x11\xcc\xdb\xc0\x16\xbe\x2d\xeb\x1e\x82\x5f\x79\xfa\xe2\x62\xa1\x30\x58\x01\x32\x99\xf4\x51\xd7\x4c\x4b\x2e\xd3\x60\x0e\x37\x12\x3b\x40\x35\xcc\xfc\x04\xd7\x6f\x98\xf0\x22\xf3\x67\xe3\x72\xa9\x2c\x95\x5d\xdd\x53\x9d\xa4\x79\x50\xeb\x1e\x87\x4c\x5a\x4f\xba\x7f\xd7\xff\xae\xe9\x42\x3b\x69\xae\x4f\x6d\x63\x0c\xb8\x27\xb9\x35\xd9\x31\x0f\xd7\x2e\xee\x9c\x97\x7b\x97\x73\xf0\xb4\x8b\x7d\x27\x4f\x39\xbb\x47\xea\x7f\xfb\x4f\x00\x00\x00\xff\xff\x30\xb3\x3d\xcd\x73\x1c\x00\x00"),
 		},
 		"/templates/email.tmpl": &vfsgen۰CompressedFileInfo{
 			name:             "email.tmpl",
diff --git a/cmd/alertmanager/main.go b/cmd/alertmanager/main.go
index 06d697a0..0337a3de 100644
--- a/cmd/alertmanager/main.go
+++ b/cmd/alertmanager/main.go
@@ -29,6 +29,7 @@ import (
 	"syscall"
 	"time"
 
+	"github.com/KimMachineGun/automemlimit/memlimit"
 	"github.com/alecthomas/kingpin/v2"
 	"github.com/go-kit/log"
 	"github.com/go-kit/log/level"
@@ -157,6 +158,9 @@ func run() int {
 		getConcurrency = kingpin.Flag("web.get-concurrency", "Maximum number of GET requests processed concurrently. If negative or zero, the limit is GOMAXPROC or 8, whichever is larger.").Default("0").Int()
 		httpTimeout    = kingpin.Flag("web.timeout", "Timeout for HTTP requests. If negative or zero, no timeout is set.").Default("0").Duration()
 
+		memlimitRatio = kingpin.Flag("auto-gomemlimit.ratio", "The ratio of reserved GOMEMLIMIT memory to the detected maximum container or system memory. The value must be greater than 0 and less than or equal to 1.").
+				Default("0.9").Float64()
+
 		clusterBindAddr = kingpin.Flag("cluster.listen-address", "Listen address for cluster. Set to empty string to disable HA mode.").
 				Default(defaultClusterAddr).String()
 		clusterAdvertiseAddr   = kingpin.Flag("cluster.advertise-address", "Explicit address to advertise in cluster.").String()
@@ -195,6 +199,25 @@ func run() int {
 	}
 	compat.InitFromFlags(logger, ff)
 
+	if ff.EnableAutoGOMEMLIMIT() {
+		if *memlimitRatio <= 0.0 || *memlimitRatio > 1.0 {
+			level.Error(logger).Log("msg", "--auto-gomemlimit.ratio must be greater than 0 and less than or equal to 1.")
+			return 1
+		}
+
+		if _, err := memlimit.SetGoMemLimitWithOpts(
+			memlimit.WithRatio(*memlimitRatio),
+			memlimit.WithProvider(
+				memlimit.ApplyFallback(
+					memlimit.FromCgroup,
+					memlimit.FromSystem,
+				),
+			),
+		); err != nil {
+			level.Warn(logger).Log("component", "automemlimit", "msg", "Failed to set GOMEMLIMIT automatically", "err", err)
+		}
+	}
+
 	if ff.EnableAutoGOMAXPROCS() {
 		l := func(format string, a ...interface{}) {
 			level.Info(logger).Log("component", "automaxprocs", "msg", fmt.Sprintf(strings.TrimPrefix(format, "maxprocs: "), a...))
diff --git a/config/config.go b/config/config.go
index 3f356996..4d694eef 100644
--- a/config/config.go
+++ b/config/config.go
@@ -263,6 +263,9 @@ func resolveFilepaths(baseDir string, cfg *Config) {
 		for _, cfg := range receiver.MSTeamsConfigs {
 			cfg.HTTPConfig.SetDirectory(baseDir)
 		}
+		for _, cfg := range receiver.JiraConfigs {
+			cfg.HTTPConfig.SetDirectory(baseDir)
+		}
 	}
 }
 
@@ -548,6 +551,17 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
 				return fmt.Errorf("no msteams webhook URL or URLFile provided")
 			}
 		}
+		for _, jira := range rcv.JiraConfigs {
+			if jira.HTTPConfig == nil {
+				jira.HTTPConfig = c.Global.HTTPConfig
+			}
+			if jira.APIURL == nil {
+				if c.Global.JiraAPIURL == nil {
+					return fmt.Errorf("no global Jira Cloud URL set")
+				}
+				jira.APIURL = c.Global.JiraAPIURL
+			}
+		}
 
 		names[rcv.Name] = struct{}{}
 	}
@@ -752,6 +766,7 @@ type GlobalConfig struct {
 
 	HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
 
+	JiraAPIURL           *URL                 `yaml:"jira_api_url,omitempty" json:"jira_api_url,omitempty"`
 	SMTPFrom             string               `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"`
 	SMTPHello            string               `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"`
 	SMTPSmarthost        HostPort             `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"`
@@ -920,6 +935,7 @@ type Receiver struct {
 	TelegramConfigs  []*TelegramConfig  `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"`
 	WebexConfigs     []*WebexConfig     `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"`
 	MSTeamsConfigs   []*MSTeamsConfig   `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"`
+	JiraConfigs      []*JiraConfig      `yaml:"jira_configs,omitempty" json:"jira_configs,omitempty"`
 }
 
 // UnmarshalYAML implements the yaml.Unmarshaler interface for Receiver.
diff --git a/config/notifiers.go b/config/notifiers.go
index 34bef22c..69841661 100644
--- a/config/notifiers.go
+++ b/config/notifiers.go
@@ -22,6 +22,7 @@ import (
 	"time"
 
 	commoncfg "github.com/prometheus/common/config"
+	"github.com/prometheus/common/model"
 	"github.com/prometheus/common/sigv4"
 )
 
@@ -172,6 +173,15 @@ var (
 		Summary: `{{ template "msteams.default.summary" . }}`,
 		Text:    `{{ template "msteams.default.text" . }}`,
 	}
+
+	DefaultJiraConfig = JiraConfig{
+		NotifierConfig: NotifierConfig{
+			VSendResolved: true,
+		},
+		Summary:     `{{ template "jira.default.summary" . }}`,
+		Description: `{{ template "jira.default.description" . }}`,
+		Priority:    `{{ template "jira.default.priority" . }}`,
+	}
 )
 
 // NotifierConfig contains base options common across all notifier configurations.
@@ -825,3 +835,41 @@ func (c *MSTeamsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
 
 	return nil
 }
+
+type JiraConfig struct {
+	NotifierConfig `yaml:",inline" json:",inline"`
+	HTTPConfig     *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`
+
+	APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
+
+	Project     string   `yaml:"project,omitempty" json:"project,omitempty"`
+	Summary     string   `yaml:"summary,omitempty" json:"summary,omitempty"`
+	Description string   `yaml:"description,omitempty" json:"description,omitempty"`
+	Labels      []string `yaml:"labels,omitempty" json:"labels,omitempty"`
+	Priority    string   `yaml:"priority,omitempty" json:"priority,omitempty"`
+	IssueType   string   `yaml:"issue_type,omitempty" json:"issue_type,omitempty"`
+
+	ReopenTransition  string         `yaml:"reopen_transition,omitempty" json:"reopen_transition,omitempty"`
+	ResolveTransition string         `yaml:"resolve_transition,omitempty" json:"resolve_transition,omitempty"`
+	WontFixResolution string         `yaml:"wont_fix_resolution,omitempty" json:"wont_fix_resolution,omitempty"`
+	ReopenDuration    model.Duration `yaml:"reopen_duration,omitempty" json:"reopen_duration,omitempty"`
+
+	Fields map[string]any `yaml:"fields,omitempty" json:"custom_fields,omitempty"`
+}
+
+func (c *JiraConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	*c = DefaultJiraConfig
+	type plain JiraConfig
+	if err := unmarshal((*plain)(c)); err != nil {
+		return err
+	}
+
+	if c.Project == "" {
+		return fmt.Errorf("missing project in jira_config")
+	}
+	if c.IssueType == "" {
+		return fmt.Errorf("missing issue_type in jira_config")
+	}
+
+	return nil
+}
diff --git a/config/receiver/receiver.go b/config/receiver/receiver.go
index 9bb039ef..040d7534 100644
--- a/config/receiver/receiver.go
+++ b/config/receiver/receiver.go
@@ -22,6 +22,7 @@ import (
 	"github.com/prometheus/alertmanager/notify"
 	"github.com/prometheus/alertmanager/notify/discord"
 	"github.com/prometheus/alertmanager/notify/email"
+	"github.com/prometheus/alertmanager/notify/jira"
 	"github.com/prometheus/alertmanager/notify/msteams"
 	"github.com/prometheus/alertmanager/notify/opsgenie"
 	"github.com/prometheus/alertmanager/notify/pagerduty"
@@ -92,6 +93,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg
 	for i, c := range nc.MSTeamsConfigs {
 		add("msteams", i, c, func(l log.Logger) (notify.Notifier, error) { return msteams.New(c, tmpl, l, httpOpts...) })
 	}
+	for i, c := range nc.JiraConfigs {
+		add("jira", i, c, func(l log.Logger) (notify.Notifier, error) { return jira.New(c, tmpl, l, httpOpts...) })
+	}
 
 	if errs.Len() > 0 {
 		return nil, &errs
diff --git a/docs/configuration.md b/docs/configuration.md
index 992e1fe4..3269b3a5 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -97,6 +97,9 @@ global:
   # The default TLS configuration for SMTP receivers
   [ smtp_tls_config: <tls_config> ]
 
+  # Default settings for the JIRA integration. 
+  [ jira_api_url: <string> ]
+
   # The API URL to use for Slack notifications.
   [ slack_api_url: <secret> ]
   [ slack_api_url_file: <filepath> ]
@@ -697,6 +700,8 @@ email_configs:
   [ - <email_config>, ... ]
 msteams_configs:
   [ - <msteams_config>, ... ]
+jira_configs:
+  [ - <jira_config>, ... ]
 opsgenie_configs:
   [ - <opsgenie_config>, ... ]
 pagerduty_configs:
@@ -944,6 +949,97 @@ Microsoft Teams notifications are sent via the [Incoming Webhooks](https://learn
 [ http_config: <http_config> | default = global.http_config ]
 ```
 
+### `<jira_config>`
+
+JIRA notifications are sent via [JIRA Rest API v2](https://developer.atlassian.com/cloud/jira/platform/rest/v2/intro/)
+or [JIRA REST API v3](https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/#version).
+
+Note: This integration is only tested against a Jira Cloud instance. 
+Jira Data Center (on premise instance) can work, but it's not guaranteed.
+
+Both APIs have the same feature set. The difference is that V2 supports [Wiki Markup](https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all)
+for the issue description and V3 supports [Atlassian Document Format (ADF)](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/).
+The default `jira.default.description` template only works with V2.
+
+```yaml
+# Whether to notify about resolved alerts.
+[ send_resolved: <boolean> | default = true ]
+
+# The URL to send API requests to. The full API path must be included.
+# Example: https://company.atlassian.net/rest/api/2/
+[ api_url: <string> | default = global.jira_api_url ]
+
+# The project key where issues are created.
+project: <string>
+
+# Issue summary template.
+[ summary: <tmpl_string> | default = '{{ template "jira.default.summary" . }}' ]
+
+# Issue description template.
+[ description: <tmpl_string> | default = '{{ template "jira.default.description" . }}' ]
+
+# Labels to be added to the issue.
+labels: 
+  [ - <tmpl_string> ... ]
+
+# Priority of the issue.
+[ priority: <tmpl_string> | default = '{{ template "jira.default.priority" . }}' ]
+
+# Type of the issue (e.g. Bug).
+[ issue_type: <string> ]
+
+# Name of the workflow transition to resolve an issue. The target status must have the category "done".
+# NOTE: The name of the transition can be localized and depends on the language setting of the service account.
+[ resolve_transition: <string> ]
+
+# Name of the workflow transition to reopen an issue. The target status should not have the category "done".
+# NOTE: The name of the transition can be localized and depends on the language setting of the service account.
+[ reopen_transition: <string> ]
+
+# If reopen_transition is defined, ignore issues with that resolution.
+[ wont_fix_resolution: <string> ]
+
+# If reopen_transition is defined, reopen the issue when it is not older than this value (rounded down to the nearest minute).
+# The resolutiondate field is used to determine the age of the issue.
+[ reopen_duration: <duration> ]
+
+# Other issue and custom fields.
+fields:
+  [ <string>: <jira_field> ... ]
+
+
+# The HTTP client's configuration. You must use this configuration to supply the personal access token (PAT) as part of the HTTP `Authorization` header.
+# For Jira Cloud, use basic_auth with the email address as the username and the PAT as the password.
+# For Jira Data Center, use the 'authorization' field with 'credentials: <PAT value>'.
+[ http_config: <http_config> | default = global.http_config ]
+```
+
+The `labels` field is a list of labels added to the issue. Template expressions are supported. For example:
+
+```yaml
+labels:
+  - 'alertmanager'
+  - '{{ .CommonLabels.severity }}'
+```
+
+#### `<jira_field>`
+
+Jira issue field can have multiple types.
+Depends on the field type, the values must be provided differently.
+See https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#setting-custom-field-data-for-other-field-types for further examples.
+
+```yaml
+fields:
+    # Components
+    components: { name: "Monitoring" }
+    # Custom Field TextField
+    customfield_10001: "Random text"
+    # Custom Field SelectList
+    customfield_10002: {"value": "red"}
+    # Custom Field MultiSelect
+    customfield_10003: [{"value": "red"}, {"value": "blue"}, {"value": "green"}]
+```
+
 ### `<opsgenie_config>`
 
 OpsGenie notifications are sent via the [OpsGenie API](https://docs.opsgenie.com/docs/alert-api).
diff --git a/featurecontrol/featurecontrol.go b/featurecontrol/featurecontrol.go
index 4616a057..e207ff8f 100644
--- a/featurecontrol/featurecontrol.go
+++ b/featurecontrol/featurecontrol.go
@@ -26,6 +26,7 @@ const (
 	FeatureReceiverNameInMetrics = "receiver-name-in-metrics"
 	FeatureClassicMode           = "classic-mode"
 	FeatureUTF8StrictMode        = "utf8-strict-mode"
+	FeatureAutoGOMEMLIMIT        = "auto-gomemlimit"
 	FeatureAutoGOMAXPROCS        = "auto-gomaxprocs"
 )
 
@@ -33,6 +34,7 @@ var AllowedFlags = []string{
 	FeatureReceiverNameInMetrics,
 	FeatureClassicMode,
 	FeatureUTF8StrictMode,
+	FeatureAutoGOMEMLIMIT,
 	FeatureAutoGOMAXPROCS,
 }
 
@@ -40,6 +42,7 @@ type Flagger interface {
 	EnableReceiverNamesInMetrics() bool
 	ClassicMode() bool
 	UTF8StrictMode() bool
+	EnableAutoGOMEMLIMIT() bool
 	EnableAutoGOMAXPROCS() bool
 }
 
@@ -48,6 +51,7 @@ type Flags struct {
 	enableReceiverNamesInMetrics bool
 	classicMode                  bool
 	utf8StrictMode               bool
+	enableAutoGOMEMLIMIT         bool
 	enableAutoGOMAXPROCS         bool
 }
 
@@ -63,6 +67,10 @@ func (f *Flags) UTF8StrictMode() bool {
 	return f.utf8StrictMode
 }
 
+func (f *Flags) EnableAutoGOMEMLIMIT() bool {
+	return f.enableAutoGOMEMLIMIT
+}
+
 func (f *Flags) EnableAutoGOMAXPROCS() bool {
 	return f.enableAutoGOMAXPROCS
 }
@@ -87,6 +95,12 @@ func enableUTF8StrictMode() flagOption {
 	}
 }
 
+func enableAutoGOMEMLIMIT() flagOption {
+	return func(configs *Flags) {
+		configs.enableAutoGOMEMLIMIT = true
+	}
+}
+
 func enableAutoGOMAXPROCS() flagOption {
 	return func(configs *Flags) {
 		configs.enableAutoGOMAXPROCS = true
@@ -112,6 +126,9 @@ func NewFlags(logger log.Logger, features string) (Flagger, error) {
 		case FeatureUTF8StrictMode:
 			opts = append(opts, enableUTF8StrictMode())
 			level.Warn(logger).Log("msg", "UTF-8 strict mode enabled")
+		case FeatureAutoGOMEMLIMIT:
+			opts = append(opts, enableAutoGOMEMLIMIT())
+			level.Warn(logger).Log("msg", "Automatically set GOMEMLIMIT to match the Linux container or system memory limit.")
 		case FeatureAutoGOMAXPROCS:
 			opts = append(opts, enableAutoGOMAXPROCS())
 			level.Warn(logger).Log("msg", "Automatically set GOMAXPROCS to match Linux container CPU quota")
@@ -139,4 +156,6 @@ func (n NoopFlags) ClassicMode() bool { return false }
 
 func (n NoopFlags) UTF8StrictMode() bool { return false }
 
+func (n NoopFlags) EnableAutoGOMEMLIMIT() bool { return false }
+
 func (n NoopFlags) EnableAutoGOMAXPROCS() bool { return false }
diff --git a/go.mod b/go.mod
index 3ecddb1f..2ac6dacf 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/prometheus/alertmanager
 go 1.21
 
 require (
+	github.com/KimMachineGun/automemlimit v0.6.1
 	github.com/alecthomas/kingpin/v2 v2.4.0
 	github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137
 	github.com/aws/aws-sdk-go v1.54.11
@@ -37,6 +38,7 @@ require (
 	github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749
 	github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546
 	github.com/stretchr/testify v1.9.0
+	github.com/trivago/tgo v1.0.7
 	github.com/xlab/treeprint v1.2.0
 	go.uber.org/atomic v1.11.0
 	go.uber.org/automaxprocs v1.5.3
@@ -52,6 +54,8 @@ require (
 	github.com/armon/go-metrics v0.3.10 // indirect
 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cilium/ebpf v0.9.1 // indirect
+	github.com/containerd/cgroups/v3 v3.0.1 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/docker/go-units v0.5.0 // indirect
@@ -60,6 +64,7 @@ require (
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.21.0 // indirect
+	github.com/godbus/dbus/v5 v5.0.4 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/btree v1.0.0 // indirect
 	github.com/google/uuid v1.6.0 // indirect
@@ -77,11 +82,14 @@ require (
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
+	github.com/opencontainers/runtime-spec v1.0.2 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
+	github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/prometheus/client_model v0.6.1 // indirect
 	github.com/prometheus/procfs v0.15.1 // indirect
 	github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
+	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
 	go.mongodb.org/mongo-driver v1.14.0 // indirect
 	go.opentelemetry.io/otel v1.24.0 // indirect
diff --git a/go.sum b/go.sum
index df7b8c9c..bab34424 100644
--- a/go.sum
+++ b/go.sum
@@ -57,6 +57,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/KimMachineGun/automemlimit v0.6.1 h1:ILa9j1onAAMadBsyyUJv5cack8Y1WT26yLj/V+ulKp8=
+github.com/KimMachineGun/automemlimit v0.6.1/go.mod h1:T7xYht7B8r6AG/AqFcUdc7fzd2bIdBKmepfP2S1svPY=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
 github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
@@ -96,6 +98,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cilium/ebpf v0.9.1 h1:64sn2K3UKw8NbP/blsixRpF3nXuyhz/VjRlRzvlBRu4=
+github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY=
 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
 github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -108,6 +112,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH
 github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/containerd/cgroups/v3 v3.0.1 h1:4hfGvu8rfGIwVIDd+nLzn/B9ZXx4BcCjzt5ToenJRaE=
+github.com/containerd/cgroups/v3 v3.0.1/go.mod h1:/vtwk1VXrtoa5AaZLkypuOJgA/6DyPMZHJPGQNtlHnw=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
@@ -133,6 +139,7 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
 github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
 github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
 github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -180,6 +187,7 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
+github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
 github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
 github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
 github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
@@ -398,11 +406,15 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
 github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
 github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/opencontainers/runtime-spec v1.0.2 h1:UfAcuLBJB9Coz72x1hgl8O5RVzTdNiaglX6v2DM6FI0=
+github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
 github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
+github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
 github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -467,6 +479,8 @@ github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJV
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo=
 github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
@@ -488,6 +502,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
+github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM=
+github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
 github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
 github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
@@ -523,6 +539,8 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
+go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
 go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
 go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
diff --git a/notify/jira/jira.go b/notify/jira/jira.go
new file mode 100644
index 00000000..51fabe2f
--- /dev/null
+++ b/notify/jira/jira.go
@@ -0,0 +1,345 @@
+// Copyright 2023 Prometheus Team
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package jira
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/go-kit/log"
+	"github.com/go-kit/log/level"
+	commoncfg "github.com/prometheus/common/config"
+	"github.com/prometheus/common/model"
+	"github.com/trivago/tgo/tcontainer"
+
+	"github.com/prometheus/alertmanager/config"
+	"github.com/prometheus/alertmanager/notify"
+	"github.com/prometheus/alertmanager/template"
+	"github.com/prometheus/alertmanager/types"
+)
+
+const (
+	maxSummaryLenRunes     = 255
+	maxDescriptionLenRunes = 32767
+)
+
+// Notifier implements a Notifier for JIRA notifications.
+type Notifier struct {
+	conf    *config.JiraConfig
+	tmpl    *template.Template
+	logger  log.Logger
+	client  *http.Client
+	retrier *notify.Retrier
+}
+
+func New(c *config.JiraConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
+	client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "jira", httpOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return &Notifier{
+		conf:    c,
+		tmpl:    t,
+		logger:  l,
+		client:  client,
+		retrier: &notify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}},
+	}, nil
+}
+
+// Notify implements the Notifier interface.
+func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
+	key, err := notify.ExtractGroupKey(ctx)
+	if err != nil {
+		return false, err
+	}
+
+	level.Debug(n.logger).Log("alert", key)
+
+	var (
+		tmplTextErr error
+
+		alerts       = types.Alerts(as...)
+		data         = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
+		tmplText     = notify.TmplText(n.tmpl, data, &tmplTextErr)
+		tmplTextFunc = func(tmpl string) (string, error) {
+			result := tmplText(tmpl)
+			return result, tmplTextErr
+		}
+
+		path   string
+		method string
+	)
+
+	existingIssue, shouldRetry, err := n.searchExistingIssue(ctx, key, alerts.Status())
+	if err != nil {
+		return shouldRetry, fmt.Errorf("error searching existing issues: %w", err)
+	}
+
+	if existingIssue == nil {
+		// Do not create new issues for resolved alerts
+		if alerts.Status() == model.AlertResolved {
+			return false, nil
+		}
+
+		level.Debug(n.logger).Log("msg", "create new issue", "alert", key.String())
+
+		path = "issue"
+		method = http.MethodPost
+	} else {
+		level.Debug(n.logger).Log("msg", "updating existing issue", "key", existingIssue.Key, "alert", key.String())
+
+		path = "issue/" + existingIssue.Key
+		method = http.MethodPut
+	}
+
+	requestBody, err := n.prepareIssueRequestBody(ctx, tmplTextFunc)
+	if err != nil {
+		return false, err
+	}
+
+	requestBody.Fields.Labels = append(requestBody.Fields.Labels, fmt.Sprintf("ALERT{%s}", key.Hash()))
+
+	sort.Strings(requestBody.Fields.Labels)
+
+	_, shouldRetry, err = n.doAPIRequest(ctx, method, path, requestBody)
+	if err != nil {
+		return shouldRetry, fmt.Errorf("error create/update existing issues: %w", err)
+	}
+
+	if existingIssue != nil && existingIssue.Key != "" && existingIssue.Fields != nil && existingIssue.Fields.Status != nil {
+		if n.conf.ResolveTransition != "" && alerts.Status() == model.AlertResolved && existingIssue.Fields.Status.StatusCategory.Key != "done" {
+			return n.transitionIssue(ctx, key, existingIssue.Key, n.conf.ResolveTransition)
+		} else if n.conf.ReopenTransition != "" && alerts.Status() == model.AlertFiring && existingIssue.Fields.Status.StatusCategory.Key == "done" {
+			return n.transitionIssue(ctx, key, existingIssue.Key, n.conf.ReopenTransition)
+		}
+	}
+
+	return false, nil
+}
+
+func (n *Notifier) prepareIssueRequestBody(ctx context.Context, tmplTextFunc templateFunc) (issue, error) {
+	summary, err := tmplTextFunc(n.conf.Summary)
+	if err != nil {
+		return issue{}, fmt.Errorf("template error: %w", err)
+	}
+
+	// Recursively convert any maps to map[string]interface{}, filtering out all non-string keys, so the json encoder
+	// doesn't blow up when marshaling JIRA requests.
+	fieldsWithStringKeys, err := tcontainer.ConvertToMarshalMap(n.conf.Fields, func(v string) string { return v })
+	if err != nil {
+		return issue{}, fmt.Errorf("convertToMarshalMap error: %w", err)
+	}
+
+	summary, truncated := notify.TruncateInRunes(summary, maxSummaryLenRunes)
+	if truncated {
+		key, err := notify.ExtractGroupKey(ctx)
+		if err != nil {
+			return issue{}, err
+		}
+		level.Warn(n.logger).Log("msg", "Truncated summary", "key", key, "max_runes", maxSummaryLenRunes)
+	}
+
+	requestBody := issue{Fields: &issueFields{
+		Project:   &issueProject{Key: n.conf.Project},
+		Issuetype: &idNameValue{Name: n.conf.IssueType},
+		Summary:   summary,
+		Labels:    make([]string, 0, len(n.conf.Labels)+1),
+		Fields:    fieldsWithStringKeys,
+	}}
+
+	issueDescriptionString, err := tmplTextFunc(n.conf.Description)
+	if err != nil {
+		return issue{}, fmt.Errorf("template error: %w", err)
+	}
+
+	issueDescriptionString, truncated = notify.TruncateInRunes(issueDescriptionString, maxDescriptionLenRunes)
+	if truncated {
+		key, err := notify.ExtractGroupKey(ctx)
+		if err != nil {
+			return issue{}, err
+		}
+		level.Warn(n.logger).Log("msg", "Truncated description", "key", key, "max_runes", maxDescriptionLenRunes)
+	}
+
+	if strings.HasSuffix(n.conf.APIURL.Path, "/3") {
+		var issueDescription any
+		if err := json.Unmarshal([]byte(issueDescriptionString), &issueDescription); err != nil {
+			return issue{}, nil
+		}
+		requestBody.Fields.Description = issueDescription
+	} else {
+		requestBody.Fields.Description = issueDescriptionString
+	}
+
+	if n.conf.Labels != nil {
+		for _, label := range n.conf.Labels {
+			label, err = tmplTextFunc(label)
+			if err != nil {
+				return issue{}, fmt.Errorf("template error: %w", err)
+			}
+			requestBody.Fields.Labels = append(requestBody.Fields.Labels, label)
+		}
+	}
+
+	priority, err := tmplTextFunc(n.conf.Priority)
+	if err != nil {
+		return issue{}, fmt.Errorf("template error: %w", err)
+	}
+
+	if priority != "" {
+		requestBody.Fields.Priority = &idNameValue{Name: priority}
+	}
+
+	return requestBody, nil
+}
+
+func (n *Notifier) searchExistingIssue(ctx context.Context, key notify.Key, status model.AlertStatus) (*issue, bool, error) {
+	jql := strings.Builder{}
+
+	if n.conf.WontFixResolution != "" {
+		jql.WriteString(fmt.Sprintf(`resolution != %q and `, n.conf.WontFixResolution))
+	}
+
+	// if the alert is firing, do not search for closed issues unless reopen transition is defined.
+	if n.conf.ReopenTransition == "" {
+		if status != model.AlertResolved {
+			jql.WriteString(`statusCategory != Done and `)
+		}
+	} else {
+		reopenDuration := int64(time.Duration(n.conf.ReopenDuration).Minutes())
+		if reopenDuration != 0 {
+			jql.WriteString(fmt.Sprintf(`(resolutiondate is EMPTY OR resolutiondate >= -%dm) and `, reopenDuration))
+		}
+	}
+
+	alertLabel := fmt.Sprintf("ALERT{%s}", key.Hash())
+	jql.WriteString(fmt.Sprintf(`project=%q and labels=%q order by status ASC,resolutiondate DESC`, n.conf.Project, alertLabel))
+
+	requestBody := issueSearch{}
+	requestBody.Jql = jql.String()
+	requestBody.MaxResults = 2
+	requestBody.Fields = []string{"status"}
+	requestBody.Expand = []string{}
+
+	level.Debug(n.logger).Log("msg", "search for recent issues", "alert", key.String(), "jql", jql.String())
+
+	responseBody, shouldRetry, err := n.doAPIRequest(ctx, http.MethodPost, "search", requestBody)
+	if err != nil {
+		return nil, shouldRetry, err
+	}
+
+	var issueSearchResult issueSearchResult
+	err = json.Unmarshal(responseBody, &issueSearchResult)
+	if err != nil {
+		return nil, false, err
+	}
+
+	if issueSearchResult.Total == 0 {
+		level.Debug(n.logger).Log("msg", "found no existing issue", "alert", key.String())
+		return nil, false, nil
+	}
+
+	if issueSearchResult.Total > 1 {
+		level.Warn(n.logger).Log("msg", "more than one issue matched, selecting the most recently resolved", "alert", key.String(), "selected", issueSearchResult.Issues[0].Key)
+	}
+
+	return &issueSearchResult.Issues[0], false, nil
+}
+
+func (n *Notifier) getIssueTransitionByName(ctx context.Context, issueKey, transitionName string) (string, bool, error) {
+	path := fmt.Sprintf("issue/%s/transitions", issueKey)
+
+	responseBody, shouldRetry, err := n.doAPIRequest(ctx, http.MethodGet, path, nil)
+	if err != nil {
+		return "", shouldRetry, err
+	}
+
+	var issueTransitions issueTransitions
+	err = json.Unmarshal(responseBody, &issueTransitions)
+	if err != nil {
+		return "", false, err
+	}
+
+	for _, issueTransition := range issueTransitions.Transitions {
+		if issueTransition.Name == transitionName {
+			return issueTransition.ID, false, nil
+		}
+	}
+
+	return "", false, fmt.Errorf("can't find transition %s for issue %s", transitionName, issueKey)
+}
+
+func (n *Notifier) transitionIssue(ctx context.Context, key notify.Key, issueKey, transitionName string) (bool, error) {
+	transitionID, shouldRetry, err := n.getIssueTransitionByName(ctx, issueKey, transitionName)
+	if err != nil {
+		return shouldRetry, err
+	}
+
+	requestBody := issue{}
+	requestBody.Transition = &idNameValue{ID: transitionID}
+
+	path := fmt.Sprintf("issue/%s/transitions", issueKey)
+
+	level.Debug(n.logger).Log("msg", "transitions jira issue", "alert", key.String(), "key", issueKey, "transition", transitionName)
+	_, shouldRetry, err = n.doAPIRequest(ctx, http.MethodPost, path, requestBody)
+
+	return shouldRetry, err
+}
+
+func (n *Notifier) doAPIRequest(ctx context.Context, method, path string, requestBody any) ([]byte, bool, error) {
+	var body io.Reader
+	if requestBody != nil {
+		var buf bytes.Buffer
+		if err := json.NewEncoder(&buf).Encode(requestBody); err != nil {
+			return nil, false, err
+		}
+
+		body = &buf
+	}
+
+	url := n.conf.APIURL.JoinPath(path)
+	req, err := http.NewRequestWithContext(ctx, method, url.String(), body)
+	if err != nil {
+		return nil, false, err
+	}
+
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept-Language", "en")
+
+	resp, err := n.client.Do(req)
+	if err != nil {
+		return nil, false, err
+	}
+
+	defer notify.Drain(resp)
+
+	responseBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, false, err
+	}
+
+	shouldRetry, err := n.retrier.Check(resp.StatusCode, bytes.NewReader(responseBody))
+	if err != nil {
+		return nil, shouldRetry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err)
+	}
+
+	return responseBody, false, nil
+}
diff --git a/notify/jira/types.go b/notify/jira/types.go
new file mode 100644
index 00000000..af5335c4
--- /dev/null
+++ b/notify/jira/types.go
@@ -0,0 +1,110 @@
+// Copyright 2023 Prometheus Team
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package jira
+
+import (
+	"encoding/json"
+)
+
+type templateFunc func(string) (string, error)
+
+type issue struct {
+	Key        string       `json:"key,omitempty"`
+	Fields     *issueFields `json:"fields,omitempty"`
+	Transition *idNameValue `json:"transition,omitempty"`
+}
+
+type issueFields struct {
+	Description any           `json:"description"`
+	Issuetype   *idNameValue  `json:"issuetype,omitempty"`
+	Labels      []string      `json:"labels,omitempty"`
+	Priority    *idNameValue  `json:"priority,omitempty"`
+	Project     *issueProject `json:"project,omitempty"`
+	Resolution  *idNameValue  `json:"resolution,omitempty"`
+	Summary     string        `json:"summary"`
+	Status      *issueStatus  `json:"status,omitempty"`
+
+	Fields map[string]any `json:"-"`
+}
+
+type idNameValue struct {
+	ID   string `json:"id,omitempty"`
+	Name string `json:"name,omitempty"`
+}
+
+type issueProject struct {
+	Key string `json:"key"`
+}
+
+type issueStatus struct {
+	Name           string `json:"name"`
+	StatusCategory struct {
+		Key string `json:"key"`
+	} `json:"statusCategory"`
+}
+
+type issueSearch struct {
+	Expand     []string `json:"expand"`
+	Fields     []string `json:"fields"`
+	Jql        string   `json:"jql"`
+	MaxResults int      `json:"maxResults"`
+	StartAt    int      `json:"startAt"`
+}
+
+type issueSearchResult struct {
+	Total  int     `json:"total"`
+	Issues []issue `json:"issues"`
+}
+
+type issueTransitions struct {
+	Transitions []idNameValue `json:"transitions"`
+}
+
+// MarshalJSON merges the struct issueFields and issueFields.CustomField together.
+func (i issueFields) MarshalJSON() ([]byte, error) {
+	jsonFields := map[string]interface{}{
+		"description": i.Description,
+		"summary":     i.Summary,
+	}
+
+	if i.Issuetype != nil {
+		jsonFields["issuetype"] = i.Issuetype
+	}
+
+	if i.Labels != nil {
+		jsonFields["labels"] = i.Labels
+	}
+
+	if i.Priority != nil {
+		jsonFields["priority"] = i.Priority
+	}
+
+	if i.Project != nil {
+		jsonFields["project"] = i.Project
+	}
+
+	if i.Resolution != nil {
+		jsonFields["resolution"] = i.Resolution
+	}
+
+	if i.Status != nil {
+		jsonFields["status"] = i.Status
+	}
+
+	for key, field := range i.Fields {
+		jsonFields[key] = field
+	}
+
+	return json.Marshal(jsonFields)
+}
diff --git a/notify/notify.go b/notify/notify.go
index d1065ab7..b5e1ae91 100644
--- a/notify/notify.go
+++ b/notify/notify.go
@@ -365,6 +365,7 @@ func (m *Metrics) InitializeFor(receiver map[string][]Integration) {
 		"discord",
 		"webex",
 		"msteams",
+		"jira",
 	} {
 		m.numNotifications.WithLabelValues(integration)
 		m.numNotificationRequestsTotal.WithLabelValues(integration)
diff --git a/template/default.tmpl b/template/default.tmpl
index 8b2bb747..299bdecb 100644
--- a/template/default.tmpl
+++ b/template/default.tmpl
@@ -158,3 +158,42 @@ Alerts Resolved:
 {{ template "__text_alert_list_markdown" .Alerts.Resolved }}
 {{ end }}
 {{ end }}
+
+{{ define "jira.default.summary" }}{{ template "__subject" . }}{{ end }}
+{{ define "jira.default.description" }}
+{{ if gt (len .Alerts.Firing) 0 }}
+# Alerts Firing:
+{{ template "__text_alert_list_markdown" .Alerts.Firing }}
+{{ end }}
+{{ if gt (len .Alerts.Resolved) 0 }}
+# Alerts Resolved:
+{{ template "__text_alert_list_markdown" .Alerts.Resolved }}
+{{ end }}
+{{ end }}
+
+{{- define "jira.default.priority" -}}
+{{- $priority := "" }}
+{{- range .Alerts.Firing -}}
+    {{- $severity := index .Labels "severity" -}}
+    {{- if (eq $severity "critical") -}}
+        {{- $priority = "High" -}}
+    {{- else if (and (eq $severity "warning") (ne $priority "High")) -}}
+        {{- $priority = "Medium" -}}
+    {{- else if (and (eq $severity "info") (eq $priority "")) -}}
+        {{- $priority = "Low" -}}
+    {{- end -}}
+{{- end -}}
+{{- if eq $priority "" -}}
+    {{- range .Alerts.Resolved -}}
+        {{- $severity := index .Labels "severity" -}}
+        {{- if (eq $severity "critical") -}}
+            {{- $priority = "High" -}}
+        {{- else if (and (eq $severity "warning") (ne $priority "High")) -}}
+            {{- $priority = "Medium" -}}
+        {{- else if (and (eq $severity "info") (eq $priority "")) -}}
+            {{- $priority = "Low" -}}
+        {{- end -}}
+    {{- end -}}
+{{- end -}}
+{{- $priority -}}
+{{- end -}}


Test patch:
diff --git a/notify/jira/jira_test.go b/notify/jira/jira_test.go
new file mode 100644
index 00000000..8e2576ca
--- /dev/null
+++ b/notify/jira/jira_test.go
@@ -0,0 +1,808 @@
+// Copyright 2023 Prometheus Team
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package jira
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+	"testing"
+	"time"
+
+	commoncfg "github.com/prometheus/common/config"
+	"github.com/prometheus/common/model"
+	"github.com/stretchr/testify/require"
+
+	"github.com/go-kit/log"
+
+	"github.com/prometheus/alertmanager/config"
+	"github.com/prometheus/alertmanager/notify"
+	"github.com/prometheus/alertmanager/notify/test"
+	"github.com/prometheus/alertmanager/template"
+	"github.com/prometheus/alertmanager/types"
+)
+
+func TestJiraRetry(t *testing.T) {
+	notifier, err := New(
+		&config.JiraConfig{
+			APIURL: &config.URL{
+				URL: &url.URL{
+					Scheme: "https",
+					Host:   "example.atlassian.net",
+					Path:   "/rest/api/2",
+				},
+			},
+			HTTPConfig: &commoncfg.HTTPClientConfig{},
+		},
+		test.CreateTmpl(t),
+		log.NewNopLogger(),
+	)
+	require.NoError(t, err)
+
+	retryCodes := append(test.DefaultRetryCodes(), http.StatusTooManyRequests)
+
+	for statusCode, expected := range test.RetryTests(retryCodes) {
+		actual, _ := notifier.retrier.Check(statusCode, nil)
+		require.Equal(t, expected, actual, fmt.Sprintf("retry - error on status %d", statusCode))
+	}
+}
+
+func TestJiraTemplating(t *testing.T) {
+	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		switch r.URL.Path {
+		case "/search":
+			w.Write([]byte(`{"total": 0, "issues": []}`))
+			return
+		default:
+			dec := json.NewDecoder(r.Body)
+			out := make(map[string]any)
+			err := dec.Decode(&out)
+			if err != nil {
+				panic(err)
+			}
+		}
+	}))
+	defer srv.Close()
+	u, _ := url.Parse(srv.URL)
+
+	for _, tc := range []struct {
+		title string
+		cfg   *config.JiraConfig
+
+		retry  bool
+		errMsg string
+	}{
+		{
+			title: "full-blown message",
+			cfg: &config.JiraConfig{
+				Summary:     `{{ template "jira.default.summary" . }}`,
+				Description: `{{ template "jira.default.description" . }}`,
+			},
+			retry: false,
+		},
+		{
+			title: "summary with templating errors",
+			cfg: &config.JiraConfig{
+				Summary: "{{ ",
+			},
+			errMsg: "template: :1: unclosed action",
+		},
+		{
+			title: "description with templating errors",
+			cfg: &config.JiraConfig{
+				Summary:     `{{ template "jira.default.summary" . }}`,
+				Description: "{{ ",
+			},
+			errMsg: "template: :1: unclosed action",
+		},
+		{
+			title: "priority with templating errors",
+			cfg: &config.JiraConfig{
+				Summary:     `{{ template "jira.default.summary" . }}`,
+				Description: `{{ template "jira.default.description" . }}`,
+				Priority:    "{{ ",
+			},
+			errMsg: "template: :1: unclosed action",
+		},
+	} {
+		tc := tc
+
+		t.Run(tc.title, func(t *testing.T) {
+			tc.cfg.APIURL = &config.URL{URL: u}
+			tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}
+			pd, err := New(tc.cfg, test.CreateTmpl(t), log.NewNopLogger())
+			require.NoError(t, err)
+
+			ctx := context.Background()
+			ctx = notify.WithGroupKey(ctx, "1")
+
+			ok, err := pd.Notify(ctx, []*types.Alert{
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"lbl1": "val1",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+			}...)
+			if tc.errMsg == "" {
+				require.NoError(t, err)
+			} else {
+				require.Error(t, err)
+				require.Contains(t, err.Error(), tc.errMsg)
+			}
+			require.Equal(t, tc.retry, ok)
+		})
+	}
+}
+
+func TestJiraNotify(t *testing.T) {
+	for _, tc := range []struct {
+		title string
+		cfg   *config.JiraConfig
+
+		alert *types.Alert
+
+		customFieldAssetFn func(t *testing.T, issue map[string]any)
+		searchResponse     issueSearchResult
+		issue              issue
+		errMsg             string
+	}{
+		{
+			title: "create new issue",
+			cfg: &config.JiraConfig{
+				Summary:           `{{ template "jira.default.summary" . }}`,
+				Description:       `{{ template "jira.default.description" . }}`,
+				IssueType:         "Incident",
+				Project:           "OPS",
+				Priority:          `{{ template "jira.default.priority" . }}`,
+				Labels:            []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
+				ReopenDuration:    model.Duration(1 * time.Hour),
+				ReopenTransition:  "REOPEN",
+				ResolveTransition: "CLOSE",
+				WontFixResolution: "WONTFIX",
+			},
+			alert: &types.Alert{
+				Alert: model.Alert{
+					Labels: model.LabelSet{
+						"alertname": "test",
+						"instance":  "vm1",
+						"severity":  "critical",
+					},
+					StartsAt: time.Now(),
+					EndsAt:   time.Now().Add(time.Hour),
+				},
+			},
+			searchResponse: issueSearchResult{
+				Total:  0,
+				Issues: []issue{},
+			},
+			issue: issue{
+				Key: "",
+				Fields: &issueFields{
+					Summary:     "[FIRING:1] test (vm1 critical)",
+					Description: "\n\n# Alerts Firing:\n\nLabels:\n  - alertname = test\n  - instance = vm1\n  - severity = critical\n\nAnnotations:\n\nSource: \n\n\n\n\n",
+					Issuetype:   &idNameValue{Name: "Incident"},
+					Labels:      []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
+					Project:     &issueProject{Key: "OPS"},
+					Priority:    &idNameValue{Name: "High"},
+				},
+			},
+			customFieldAssetFn: func(t *testing.T, issue map[string]any) {},
+			errMsg:             "",
+		},
+		{
+			title: "create new issue with custom field and too long summary",
+			cfg: &config.JiraConfig{
+				Summary:     strings.Repeat("A", maxSummaryLenRunes+10),
+				Description: `{{ template "jira.default.description" . }}`,
+				IssueType:   "Incident",
+				Project:     "OPS",
+				Priority:    `{{ template "jira.default.priority" . }}`,
+				Labels:      []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
+				Fields: map[string]any{
+					"components":        map[any]any{"name": "Monitoring"},
+					"customfield_10001": "value",
+					"customfield_10002": 0,
+					"customfield_10003": []any{0},
+					"customfield_10004": map[any]any{"value": "red"},
+					"customfield_10005": map[any]any{"value": 0},
+					"customfield_10006": []map[any]any{{"value": "red"}, {"value": "blue"}, {"value": "green"}},
+					"customfield_10007": []map[any]any{{"value": "red"}, {"value": "blue"}, {"value": 0}},
+					"customfield_10008": []map[any]any{{"value": 0}, {"value": 1}, {"value": 2}},
+				},
+				ReopenDuration:    model.Duration(1 * time.Hour),
+				ReopenTransition:  "REOPEN",
+				ResolveTransition: "CLOSE",
+				WontFixResolution: "WONTFIX",
+			},
+			alert: &types.Alert{
+				Alert: model.Alert{
+					Labels: model.LabelSet{
+						"alertname": "test",
+						"instance":  "vm1",
+					},
+					StartsAt: time.Now(),
+					EndsAt:   time.Now().Add(time.Hour),
+				},
+			},
+			searchResponse: issueSearchResult{
+				Total:  0,
+				Issues: []issue{},
+			},
+			issue: issue{
+				Key: "",
+				Fields: &issueFields{
+					Summary:     strings.Repeat("A", maxSummaryLenRunes-1) + "…",
+					Description: "\n\n# Alerts Firing:\n\nLabels:\n  - alertname = test\n  - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n",
+					Issuetype:   &idNameValue{Name: "Incident"},
+					Labels:      []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
+					Project:     &issueProject{Key: "OPS"},
+				},
+			},
+			customFieldAssetFn: func(t *testing.T, issue map[string]any) {
+				require.Equal(t, "value", issue["customfield_10001"])
+				require.Equal(t, float64(0), issue["customfield_10002"])
+				require.Equal(t, []any{float64(0)}, issue["customfield_10003"])
+				require.Equal(t, map[string]any{"value": "red"}, issue["customfield_10004"])
+				require.Equal(t, map[string]any{"value": float64(0)}, issue["customfield_10005"])
+				require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": "green"}}, issue["customfield_10006"])
+				require.Equal(t, []any{map[string]any{"value": "red"}, map[string]any{"value": "blue"}, map[string]any{"value": float64(0)}}, issue["customfield_10007"])
+				require.Equal(t, []any{map[string]any{"value": float64(0)}, map[string]any{"value": float64(1)}, map[string]any{"value": float64(2)}}, issue["customfield_10008"])
+			},
+			errMsg: "",
+		},
+		{
+			title: "reopen issue",
+			cfg: &config.JiraConfig{
+				Summary:           `{{ template "jira.default.summary" . }}`,
+				Description:       `{{ template "jira.default.description" . }}`,
+				IssueType:         "Incident",
+				Project:           "OPS",
+				Priority:          `{{ template "jira.default.priority" . }}`,
+				Labels:            []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
+				ReopenDuration:    model.Duration(1 * time.Hour),
+				ReopenTransition:  "REOPEN",
+				ResolveTransition: "CLOSE",
+				WontFixResolution: "WONTFIX",
+			},
+			alert: &types.Alert{
+				Alert: model.Alert{
+					Labels: model.LabelSet{
+						"alertname": "test",
+						"instance":  "vm1",
+					},
+					StartsAt: time.Now(),
+					EndsAt:   time.Now().Add(time.Hour),
+				},
+			},
+			searchResponse: issueSearchResult{
+				Total: 1,
+				Issues: []issue{
+					{
+						Key: "OPS-1",
+						Fields: &issueFields{
+							Status: &issueStatus{
+								Name: "Closed",
+								StatusCategory: struct {
+									Key string `json:"key"`
+								}{
+									Key: "done",
+								},
+							},
+						},
+					},
+				},
+			},
+			issue: issue{
+				Key: "",
+				Fields: &issueFields{
+					Summary:     "[FIRING:1] test (vm1)",
+					Description: "\n\n# Alerts Firing:\n\nLabels:\n  - alertname = test\n  - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n",
+					Issuetype:   &idNameValue{Name: "Incident"},
+					Labels:      []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
+					Project:     &issueProject{Key: "OPS"},
+					Priority:    &idNameValue{Name: "High"},
+				},
+			},
+			customFieldAssetFn: func(t *testing.T, issue map[string]any) {},
+			errMsg:             "",
+		},
+		{
+			title: "error resolve transition not found",
+			cfg: &config.JiraConfig{
+				Summary:           `{{ template "jira.default.summary" . }}`,
+				Description:       `{{ template "jira.default.description" . }}`,
+				IssueType:         "Incident",
+				Project:           "OPS",
+				Priority:          `{{ template "jira.default.priority" . }}`,
+				Labels:            []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
+				ReopenDuration:    model.Duration(1 * time.Hour),
+				ReopenTransition:  "REOPEN",
+				ResolveTransition: "CLOSE",
+				WontFixResolution: "WONTFIX",
+			},
+			alert: &types.Alert{
+				Alert: model.Alert{
+					Labels: model.LabelSet{
+						"alertname": "test",
+						"instance":  "vm1",
+					},
+					StartsAt: time.Now().Add(-time.Hour),
+					EndsAt:   time.Now().Add(-time.Hour),
+				},
+			},
+			searchResponse: issueSearchResult{
+				Total: 1,
+				Issues: []issue{
+					{
+						Key: "OPS-3",
+						Fields: &issueFields{
+							Status: &issueStatus{
+								Name: "Open",
+								StatusCategory: struct {
+									Key string `json:"key"`
+								}{
+									Key: "open",
+								},
+							},
+						},
+					},
+				},
+			},
+			issue: issue{
+				Key: "",
+				Fields: &issueFields{
+					Summary:     "[RESOLVED] test (vm1)",
+					Description: "\n\n\n# Alerts Resolved:\n\nLabels:\n  - alertname = test\n  - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n",
+					Issuetype:   &idNameValue{Name: "Incident"},
+					Labels:      []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
+					Project:     &issueProject{Key: "OPS"},
+				},
+			},
+			customFieldAssetFn: func(t *testing.T, issue map[string]any) {},
+			errMsg:             "can't find transition CLOSE for issue OPS-3",
+		},
+		{
+			title: "error reopen transition not found",
+			cfg: &config.JiraConfig{
+				Summary:           `{{ template "jira.default.summary" . }}`,
+				Description:       `{{ template "jira.default.description" . }}`,
+				IssueType:         "Incident",
+				Project:           "OPS",
+				Priority:          `{{ template "jira.default.priority" . }}`,
+				Labels:            []string{"alertmanager", "{{ .GroupLabels.alertname }}"},
+				ReopenDuration:    model.Duration(1 * time.Hour),
+				ReopenTransition:  "REOPEN",
+				ResolveTransition: "CLOSE",
+				WontFixResolution: "WONTFIX",
+			},
+			alert: &types.Alert{
+				Alert: model.Alert{
+					Labels: model.LabelSet{
+						"alertname": "test",
+						"instance":  "vm1",
+					},
+					StartsAt: time.Now(),
+					EndsAt:   time.Now().Add(time.Hour),
+				},
+			},
+			searchResponse: issueSearchResult{
+				Total: 1,
+				Issues: []issue{
+					{
+						Key: "OPS-3",
+						Fields: &issueFields{
+							Status: &issueStatus{
+								Name: "Closed",
+								StatusCategory: struct {
+									Key string `json:"key"`
+								}{
+									Key: "done",
+								},
+							},
+						},
+					},
+				},
+			},
+			issue: issue{
+				Key: "",
+				Fields: &issueFields{
+					Summary:     "[FIRING:1] test (vm1)",
+					Description: "\n\n# Alerts Firing:\n\nLabels:\n  - alertname = test\n  - instance = vm1\n\nAnnotations:\n\nSource: \n\n\n\n\n",
+					Issuetype:   &idNameValue{Name: "Incident"},
+					Labels:      []string{"ALERT{6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b}", "alertmanager", "test"},
+					Project:     &issueProject{Key: "OPS"},
+				},
+			},
+			customFieldAssetFn: func(t *testing.T, issue map[string]any) {},
+			errMsg:             "can't find transition REOPEN for issue OPS-3",
+		},
+	} {
+		tc := tc
+
+		t.Run(tc.title, func(t *testing.T) {
+			srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				switch r.URL.Path {
+				case "/search":
+					enc := json.NewEncoder(w)
+					if err := enc.Encode(tc.searchResponse); err != nil {
+						panic(err)
+					}
+
+					return
+				case "/issue/OPS-1/transitions":
+					switch r.Method {
+					case http.MethodGet:
+						w.WriteHeader(http.StatusOK)
+
+						transitions := issueTransitions{
+							Transitions: []idNameValue{
+								{ID: "12345", Name: "REOPEN"},
+							},
+						}
+
+						enc := json.NewEncoder(w)
+						if err := enc.Encode(transitions); err != nil {
+							panic(err)
+						}
+					case http.MethodPost:
+						dec := json.NewDecoder(r.Body)
+						var out issue
+						err := dec.Decode(&out)
+						if err != nil {
+							panic(err)
+						}
+
+						require.Equal(t, issue{Transition: &idNameValue{ID: "12345"}}, out)
+						w.WriteHeader(http.StatusNoContent)
+					default:
+						t.Fatalf("unexpected method %s", r.Method)
+					}
+
+					return
+				case "/issue/OPS-2/transitions":
+					switch r.Method {
+					case http.MethodGet:
+						w.WriteHeader(http.StatusOK)
+
+						transitions := issueTransitions{
+							Transitions: []idNameValue{
+								{ID: "54321", Name: "CLOSE"},
+							},
+						}
+
+						enc := json.NewEncoder(w)
+						if err := enc.Encode(transitions); err != nil {
+							panic(err)
+						}
+					case http.MethodPost:
+						dec := json.NewDecoder(r.Body)
+						var out issue
+						err := dec.Decode(&out)
+						if err != nil {
+							panic(err)
+						}
+
+						require.Equal(t, issue{Transition: &idNameValue{ID: "54321"}}, out)
+						w.WriteHeader(http.StatusNoContent)
+					default:
+						t.Fatalf("unexpected method %s", r.Method)
+					}
+
+					return
+				case "/issue/OPS-3/transitions":
+					switch r.Method {
+					case http.MethodGet:
+						w.WriteHeader(http.StatusOK)
+
+						transitions := issueTransitions{
+							Transitions: []idNameValue{},
+						}
+
+						enc := json.NewEncoder(w)
+						if err := enc.Encode(transitions); err != nil {
+							panic(err)
+						}
+					default:
+						t.Fatalf("unexpected method %s", r.Method)
+					}
+
+					return
+				case "/issue/OPS-1":
+				case "/issue/OPS-2":
+				case "/issue/OPS-3":
+					fallthrough
+				case "/issue":
+					body, err := io.ReadAll(r.Body)
+					if err != nil {
+						panic(err)
+					}
+
+					var (
+						issue issue
+						raw   map[string]any
+					)
+
+					if err := json.Unmarshal(body, &issue); err != nil {
+						panic(err)
+					}
+
+					// We don't care about the key, so copy it over.
+					issue.Fields.Fields = tc.issue.Fields.Fields
+
+					require.Equal(t, tc.issue.Key, issue.Key)
+					require.Equal(t, tc.issue.Fields, issue.Fields)
+
+					if err := json.Unmarshal(body, &raw); err != nil {
+						panic(err)
+					}
+
+					if fields, ok := raw["fields"].(map[string]any); ok {
+						tc.customFieldAssetFn(t, fields)
+					} else {
+						t.Errorf("fields should a map of string")
+					}
+
+					w.WriteHeader(http.StatusCreated)
+
+					w.WriteHeader(http.StatusCreated)
+
+				default:
+					t.Fatalf("unexpected path %s", r.URL.Path)
+				}
+			}))
+			defer srv.Close()
+			u, _ := url.Parse(srv.URL)
+
+			tc.cfg.APIURL = &config.URL{URL: u}
+			tc.cfg.HTTPConfig = &commoncfg.HTTPClientConfig{}
+
+			notifier, err := New(tc.cfg, test.CreateTmpl(t), log.NewNopLogger())
+			require.NoError(t, err)
+
+			ctx := context.Background()
+			ctx = notify.WithGroupKey(ctx, "1")
+			ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": "test"})
+
+			_, err = notifier.Notify(ctx, tc.alert)
+			if tc.errMsg == "" {
+				require.NoError(t, err)
+			} else {
+				require.Error(t, err)
+				require.EqualError(t, err, tc.errMsg)
+			}
+		})
+	}
+}
+
+func TestJiraPriority(t *testing.T) {
+	t.Parallel()
+	for _, tc := range []struct {
+		title string
+
+		alerts []*types.Alert
+
+		expectedPriority string
+	}{
+		{
+			"empty",
+			[]*types.Alert{
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+			},
+			"",
+		},
+		{
+			"critical",
+			[]*types.Alert{
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "critical",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+			},
+			"High",
+		},
+		{
+			"warning",
+			[]*types.Alert{
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "warning",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+			},
+			"Medium",
+		},
+		{
+			"info",
+			[]*types.Alert{
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "info",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+			},
+			"Low",
+		},
+		{
+			"critical+warning+info",
+			[]*types.Alert{
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "critical",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "warning",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "info",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+			},
+			"High",
+		},
+		{
+			"warning+info",
+			[]*types.Alert{
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "warning",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "info",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+			},
+			"Medium",
+		},
+		{
+			"critical(resolved)+warning+info",
+			[]*types.Alert{
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "critical",
+						},
+						StartsAt: time.Now().Add(-time.Hour),
+						EndsAt:   time.Now().Add(-time.Hour),
+					},
+				},
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "warning",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+				{
+					Alert: model.Alert{
+						Labels: model.LabelSet{
+							"alertname": "test",
+							"instance":  "vm1",
+							"severity":  "info",
+						},
+						StartsAt: time.Now(),
+						EndsAt:   time.Now().Add(time.Hour),
+					},
+				},
+			},
+			"Medium",
+		},
+	} {
+		tc := tc
+
+		t.Run(tc.title, func(t *testing.T) {
+			t.Parallel()
+			u, err := url.Parse("http://example.com/")
+			require.NoError(t, err)
+
+			tmpl, err := template.FromGlobs([]string{})
+			require.NoError(t, err)
+
+			tmpl.ExternalURL = u
+
+			var (
+				data = tmpl.Data("jira", model.LabelSet{}, tc.alerts...)
+
+				tmplTextErr  error
+				tmplText     = notify.TmplText(tmpl, data, &tmplTextErr)
+				tmplTextFunc = func(tmpl string) (string, error) {
+					result := tmplText(tmpl)
+					return result, tmplTextErr
+				}
+			)
+
+			priority, err := tmplTextFunc(`{{ template "jira.default.priority" . }}`)
+			require.NoError(t, err)
+			require.Equal(t, tc.expectedPriority, priority)
+		})
+	}
+}