From 85765cbd01412030cefa3c6938872b05610b09ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:07:18 +0000 Subject: [PATCH 1/3] fix(deps): update all dependencies Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/release.yml | 6 +++--- Makefile | 8 ++++---- go.mod | 4 +--- go.sum | 13 ++----------- 5 files changed, 17 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb7c250..0cebfc3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: "1.26.7" + go-version: "1.27.1" cache: true - name: Cache Go tools @@ -45,7 +45,7 @@ jobs: - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: "1.26.7" + go-version: "1.27.1" cache: true - name: Cache Go tools @@ -93,7 +93,7 @@ jobs: - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: "1.26.7" + go-version: "1.27.1" cache: true - name: Cache Go tools @@ -113,7 +113,7 @@ jobs: runs-on: ubuntu-latest services: sonarqube: - image: docker.io/library/sonarqube:26.7.0.124771-community@sha256:160bd2f6a3485bd09b655ef22dd63c02bd1fa7ba82aa5d9973fd010b8bcca0b3 + image: docker.io/library/sonarqube:26.8.0.126808-community@sha256:9026624a61cd25542a402a9e7213dd7dbb39724ac9597e331e6b85362558c079 ports: - 9000:9000 env: @@ -125,7 +125,7 @@ jobs: - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: "1.26.7" + go-version: "1.27.1" cache: true - name: Cache Go tools @@ -161,7 +161,7 @@ jobs: - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: "1.26.7" + go-version: "1.27.1" cache: true - name: Cache Go tools @@ -189,7 +189,7 @@ jobs: - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: "1.26.7" + go-version: "1.27.1" - name: Build run: make build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d120cd..c9486ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: fetch-depth: 0 - name: Generate a changelog - uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4.8.0 + uses: orhun/git-cliff-action@3d96a18cc4ec17e9dc69ddcc424ccafaf1f78ce2 # v4.9.0 id: git-cliff with: config: cliff.toml @@ -29,7 +29,7 @@ jobs: GITHUB_REPO: ${{ github.repository }} - name: Create GitHub Release - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 id: create_release with: body: ${{ steps.git-cliff.outputs.content }} @@ -55,7 +55,7 @@ jobs: - name: Set up Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: - go-version: "1.26.7" + go-version: "1.27.1" - name: Build run: make build diff --git a/Makefile b/Makefile index d145452..e0729ef 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ enterprise_endpoint := http://127.0.0.1:9001 username := admin password := admin # renovate: datasource=docker depName=docker.io/library/sonarqube versioning=docker -sonarqube_version := 26.7.0.124771-community +sonarqube_version := 26.8.0.126808-community # renovate: datasource=docker depName=docker.io/library/sonarqube versioning=docker sonarqube_enterprise_version := 2026.4.1-enterprise @@ -15,11 +15,11 @@ sonarqube_enterprise_version := 2026.4.1-enterprise # renovate: datasource=go depName=gotest.tools/gotestsum gotestsum_version := v1.13.0 # renovate: datasource=go depName=github.com/onsi/ginkgo/v2 -ginkgo_version := v2.32.0 +ginkgo_version := v2.32.1 # renovate: datasource=go depName=github.com/golangci/golangci-lint/v2 -golangci_lint_version := v2.12.2 +golangci_lint_version := v2.13.2 # renovate: datasource=go depName=golang.org/x/vuln -govulncheck_version := v1.3.0 +govulncheck_version := v1.7.0 version := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") build_time := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) diff --git a/go.mod b/go.mod index 4e42d31..6c40162 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.26.7 require ( github.com/google/go-querystring v1.2.0 github.com/onsi/ginkgo/v2 v2.32.1 - github.com/onsi/gomega v1.42.1 + github.com/onsi/gomega v1.43.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.12.1 @@ -15,13 +15,11 @@ require ( require ( github.com/Masterminds/semver/v3 v3.5.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20260604005048-7023385849c0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/mod v0.38.0 // indirect diff --git a/go.sum b/go.sum index d96ed02..340929d 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= @@ -34,14 +32,10 @@ github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= -github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E= -github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw= github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= -github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= -github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/onsi/gomega v1.43.0 h1:VlG/1FxqNxhSO+lq/OHBNaaqwiBK/mO8JbVkX9Y+FeU= +github.com/onsi/gomega v1.43.0/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -50,8 +44,6 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= @@ -68,7 +60,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= From 6f552bc92b0013c3ba94072cfb2d733fcdb9e43d Mon Sep 17 00:00:00 2001 From: BoxBoxJason Date: Thu, 10 Sep 2026 22:11:07 +0200 Subject: [PATCH 2/3] feat: support 26.8.0.126808-community features Signed-off-by: BoxBoxJason --- .golangci.yml | 2 + assets/api.enterprise.json | 34 +- assets/api.json | 39 +- assets/api.v2.json | 1328 ++++++++++++++++- .../v2_dop_translation_test.go | 46 + integration_testing/v2_history_test.go | 218 +++ sonar/client.go | 3 + sonar/dop_translation_v2_service.go | 56 + sonar/dop_translation_v2_service_test.go | 44 + sonar/history_v2_service.go | 488 ++++++ sonar/history_v2_service_test.go | 274 ++++ 11 files changed, 2468 insertions(+), 64 deletions(-) create mode 100644 integration_testing/v2_history_test.go create mode 100644 sonar/history_v2_service.go create mode 100644 sonar/history_v2_service_test.go diff --git a/.golangci.yml b/.golangci.yml index 0227c16..ee29e5d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -29,7 +29,9 @@ linters: - depguard - lll - err113 + - exhaustruct_v5 # v2.13 rename of exhaustruct; keep the stable name so //nolint:exhaustruct directives still apply - gomodguard + - modernize # stylistic-only suggestions (e.g. reflect.TypeAssert), too noisy for this codebase - nonamedreturns - revive - wsl diff --git a/assets/api.enterprise.json b/assets/api.enterprise.json index 953d52b..c73be66 100644 --- a/assets/api.enterprise.json +++ b/assets/api.enterprise.json @@ -6261,15 +6261,15 @@ "required": false, "internal": false, "possibleValues": [ - "FILE_LINE", - "STATUS", - "SEVERITY", "CREATION_DATE", "HOTSPOTS", "TYPE_SEVERITY", "UPDATE_DATE", "CLOSE_DATE", - "QUALITY_SEVERITY" + "QUALITY_SEVERITY", + "FILE_LINE", + "STATUS", + "SEVERITY" ] }, { @@ -7103,7 +7103,7 @@ }, { "key": "metricKeys", - "description": "Comma-separated list of metric keys. Types DISTRIB are not allowed. For type DATA only security_issues, maintainability_issues, reliability_issues, new_maintainability_issues, new_security_issues, new_reliability_issues metrics are supported", + "description": "Comma-separated list of metric keys. Types DISTRIB are not allowed. For type DATA only maintainability_issues, reliability_issues, new_maintainability_issues, new_security_issues, new_reliability_issues, security_issues metrics are supported", "required": true, "internal": false, "exampleValue": "ncloc,complexity,violations", @@ -7207,9 +7207,9 @@ "internal": false, "defaultValue": "all", "possibleValues": [ + "all", "children", - "leaves", - "all" + "leaves" ] } ] @@ -11912,12 +11912,12 @@ "description": "Sort field", "required": false, "internal": false, - "exampleValue": "key", + "exampleValue": "name", "possibleValues": [ - "key", "name", "createdAt", - "updatedAt" + "updatedAt", + "key" ] }, { @@ -13048,12 +13048,12 @@ "description": "Sort field", "required": false, "internal": false, - "exampleValue": "key", + "exampleValue": "name", "possibleValues": [ - "key", "name", "createdAt", - "updatedAt" + "updatedAt", + "key" ] }, { @@ -14912,7 +14912,7 @@ "description": "Comma-separated list of additional fields to be returned in the response. All the fields are returned by default, except actives.", "required": false, "internal": false, - "exampleValue": "repo,status", + "exampleValue": "sysTags,gapDescription", "possibleValues": [ "actives", "cleanCodeAttribute", @@ -15170,12 +15170,12 @@ "description": "Sort field", "required": false, "internal": false, - "exampleValue": "key", + "exampleValue": "name", "possibleValues": [ - "key", "name", "createdAt", - "updatedAt" + "updatedAt", + "key" ] }, { diff --git a/assets/api.json b/assets/api.json index 9fa1aa8..6848185 100644 --- a/assets/api.json +++ b/assets/api.json @@ -1616,6 +1616,7 @@ "REPORT", "ISSUE_SYNC", "AUDIT_PURGE", + "HISTORY_PURGE", "PROJECT_EXPORT" ] } @@ -5139,15 +5140,15 @@ "required": false, "internal": false, "possibleValues": [ - "FILE_LINE", - "STATUS", - "SEVERITY", - "CREATION_DATE", "HOTSPOTS", - "TYPE_SEVERITY", - "UPDATE_DATE", + "CREATION_DATE", + "SEVERITY", + "STATUS", + "FILE_LINE", + "QUALITY_SEVERITY", "CLOSE_DATE", - "QUALITY_SEVERITY" + "UPDATE_DATE", + "TYPE_SEVERITY" ] }, { @@ -5981,7 +5982,7 @@ }, { "key": "metricKeys", - "description": "Comma-separated list of metric keys. Types DISTRIB are not allowed. For type DATA only security_issues, maintainability_issues, reliability_issues, new_maintainability_issues, new_security_issues, new_reliability_issues metrics are supported", + "description": "Comma-separated list of metric keys. Types DISTRIB are not allowed. For type DATA only maintainability_issues, security_issues, new_reliability_issues, new_security_issues, new_maintainability_issues, reliability_issues metrics are supported", "required": true, "internal": false, "exampleValue": "ncloc,complexity,violations", @@ -6083,8 +6084,8 @@ "defaultValue": "all", "possibleValues": [ "leaves", - "all", - "children" + "children", + "all" ] } ] @@ -9256,7 +9257,7 @@ }, { "key": "metric", - "description": "Condition metric.\u003cbr/\u003e Only metric of the following types are allowed:\u003cul\u003e\u003cli\u003eINT\u003c/li\u003e\u003cli\u003eMILLISEC\u003c/li\u003e\u003cli\u003eRATING\u003c/li\u003e\u003cli\u003eWORK_DUR\u003c/li\u003e\u003cli\u003eFLOAT\u003c/li\u003e\u003cli\u003ePERCENT\u003c/li\u003e\u003cli\u003eLEVEL\u003c/li\u003e\u003c/ul\u003eFollowing metrics are forbidden:\u003cul\u003e\u003cli\u003ealert_status\u003c/li\u003e\u003cli\u003esecurity_hotspots\u003c/li\u003e\u003cli\u003enew_security_hotspots\u003c/li\u003e\u003c/ul\u003e", + "description": "Condition metric.\u003cbr/\u003e Only metric of the following types are allowed:\u003cul\u003e\u003cli\u003eINT\u003c/li\u003e\u003cli\u003eMILLISEC\u003c/li\u003e\u003cli\u003eRATING\u003c/li\u003e\u003cli\u003eWORK_DUR\u003c/li\u003e\u003cli\u003eFLOAT\u003c/li\u003e\u003cli\u003ePERCENT\u003c/li\u003e\u003cli\u003eLEVEL\u003c/li\u003e\u003c/ul\u003eFollowing metrics are forbidden:\u003cul\u003e\u003cli\u003enew_security_hotspots\u003c/li\u003e\u003cli\u003esecurity_hotspots\u003c/li\u003e\u003cli\u003ealert_status\u003c/li\u003e\u003c/ul\u003e", "required": true, "internal": false, "exampleValue": "blocker_violations, vulnerabilities, new_code_smells" @@ -10027,7 +10028,7 @@ }, { "key": "metric", - "description": "Condition metric.\u003cbr/\u003e Only metric of the following types are allowed:\u003cul\u003e\u003cli\u003eINT\u003c/li\u003e\u003cli\u003eMILLISEC\u003c/li\u003e\u003cli\u003eRATING\u003c/li\u003e\u003cli\u003eWORK_DUR\u003c/li\u003e\u003cli\u003eFLOAT\u003c/li\u003e\u003cli\u003ePERCENT\u003c/li\u003e\u003cli\u003eLEVEL\u003c/li\u003e\u003c/ul\u003eFollowing metrics are forbidden:\u003cul\u003e\u003cli\u003ealert_status\u003c/li\u003e\u003cli\u003esecurity_hotspots\u003c/li\u003e\u003cli\u003enew_security_hotspots\u003c/li\u003e\u003c/ul\u003e", + "description": "Condition metric.\u003cbr/\u003e Only metric of the following types are allowed:\u003cul\u003e\u003cli\u003eINT\u003c/li\u003e\u003cli\u003eMILLISEC\u003c/li\u003e\u003cli\u003eRATING\u003c/li\u003e\u003cli\u003eWORK_DUR\u003c/li\u003e\u003cli\u003eFLOAT\u003c/li\u003e\u003cli\u003ePERCENT\u003c/li\u003e\u003cli\u003eLEVEL\u003c/li\u003e\u003c/ul\u003eFollowing metrics are forbidden:\u003cul\u003e\u003cli\u003enew_security_hotspots\u003c/li\u003e\u003cli\u003esecurity_hotspots\u003c/li\u003e\u003cli\u003ealert_status\u003c/li\u003e\u003c/ul\u003e", "required": true, "internal": false, "exampleValue": "blocker_violations, vulnerabilities, new_code_smells" @@ -10428,9 +10429,9 @@ "exampleValue": "key", "possibleValues": [ "key", - "name", + "updatedAt", "createdAt", - "updatedAt" + "name" ] }, { @@ -11410,9 +11411,9 @@ "exampleValue": "key", "possibleValues": [ "key", - "name", + "updatedAt", "createdAt", - "updatedAt" + "name" ] }, { @@ -13019,7 +13020,7 @@ "description": "Comma-separated list of additional fields to be returned in the response. All the fields are returned by default, except actives.", "required": false, "internal": false, - "exampleValue": "createdAt,remFnOverloaded", + "exampleValue": "updatedAt,deprecatedKeys", "possibleValues": [ "actives", "cleanCodeAttribute", @@ -13280,9 +13281,9 @@ "exampleValue": "key", "possibleValues": [ "key", - "name", + "updatedAt", "createdAt", - "updatedAt" + "name" ] }, { diff --git a/assets/api.v2.json b/assets/api.v2.json index 8946887..2c1a610 100644 --- a/assets/api.v2.json +++ b/assets/api.v2.json @@ -3,7 +3,7 @@ "info": { "title": "SonarQube Web API v2", "description": "The SonarQube API v2 is a REST API which enables you to interact with SonarQube programmatically.\nWhile not all endpoints of the former Web API are available yet, the ones available are stable and can be used in production environments.\n", - "version": "26.7.0.124771" + "version": "26.8.0.126808" }, "servers": [ { @@ -11,6 +11,24 @@ "description": "Generated server url" } ], + "tags": [ + { + "name": "project-measures", + "description": "the project-measures API" + }, + { + "name": "measures-history", + "description": "the measures-history API" + }, + { + "name": "issue-count-history", + "description": "the issue-count-history API" + }, + { + "name": "project-issue-counts", + "description": "the project-issue-counts API" + } + ], "paths": { "/dop-translation/bound-projects": { "put": { @@ -18,7 +36,7 @@ "Dop Translation" ], "summary": "Create or update a bound SonarQube project", - "description": "Create a SonarQube project bound to a DevOps platform repository, or update the binding if the project already exists.\nThis is an idempotent operation. If the project already exists with the same key, its binding will be updated.\nAutoconfigure Pull-Request decoration mechanism.\nRequires the 'Create Projects' permission and setting a Personal Access Token with api/alm_integrations/set_pat for a user who will be using this endpoint\n", + "description": "Create a SonarQube project bound to a DevOps platform repository, or update the binding if the project already exists.\nThis is an idempotent operation. If the project already exists with the same key, its binding will be updated.\nAutoconfigure Pull-Request decoration mechanism.\nCreating a new project requires the 'Create Projects' permission and setting a Personal Access Token with\napi/alm_integrations/set_pat for a user who will be using this endpoint.\nUpdating an existing project additionally requires the 'Administer' permission on that project.\n", "operationId": "createOrUpdateBoundProject", "requestBody": { "content": { @@ -50,6 +68,9 @@ "403": { "description": "Insufficient permissions - requires 'Create Projects' permission" }, + "404": { + "description": "Project already exists and the caller lacks 'Administer' permission on it" + }, "500": { "description": "Internal server error - failed to fetch repository from DevOps platform (e.g., repository not found, invalid PAT, or insufficient permissions)" } @@ -435,6 +456,40 @@ "x-sonar-internal": "true" } }, + "/dop-translation/github-installation-tokens": { + "post": { + "tags": [ + "Dop Translation" + ], + "summary": "Mint a GitHub App installation token", + "description": "Mint a short-lived GitHub App installation token, scoped to the bound repository of the given\nproject. Internal endpoint used by the agentic-workflows remediation orchestrator (SONAR-30903).\nRequires the 'Administer System' permission.\n", + "operationId": "generateInstallationToken", + "parameters": [ + { + "name": "project", + "in": "query", + "description": "Project key", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GithubInstallationTokenRestResponse" + } + } + } + } + }, + "x-sonar-internal": "true" + } + }, "/dop-translation/github-configurations": { "get": { "tags": [ @@ -1356,40 +1411,947 @@ "204": { "description": "This SonarQube node is alive" }, - "default": { - "description": "This SonarQube node is not alive and should be rescheduled" - } - } - } - }, - "/system/health": { - "get": { - "tags": [ - "System" - ], - "operationId": "getHealth", - "parameters": [ - { - "name": "X-Sonar-Passcode", - "in": "header", - "required": false, - "schema": { - "type": "string" + "default": { + "description": "This SonarQube node is not alive and should be rescheduled" + } + } + } + }, + "/system/health": { + "get": { + "tags": [ + "System" + ], + "operationId": "getHealth", + "parameters": [ + { + "name": "X-Sonar-Passcode", + "in": "header", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Health" + } + } + } + } + } + } + }, + "/history/project-measures": { + "get": { + "tags": [ + "project-measures" + ], + "summary": "Get current project measure values.", + "description": "Get current measure values for the flattened list of projects in a portfolio or application. Identify the selection with either the legacy portfolioId parameter, or with both entityType and entityId. The two selector forms are mutually exclusive. ", + "operationId": "getProjectMeasures", + "parameters": [ + { + "name": "metricKey", + "in": "query", + "description": "Metric key", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "metricValue", + "in": "query", + "description": "Filter by metric value (exact match). Only supports primitive metric types. It does not support metrics of type DISTR or DATA. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "nameContains", + "in": "query", + "description": "Search query for project names, case insensitive", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "pageIndex", + "in": "query", + "description": "Index of the page to fetch", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 1, + "minimum": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "description": "Size of the page to fetch.", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50, + "minimum": 0 + } + }, + { + "name": "portfolioId", + "in": "query", + "description": "Legacy portfolio ID. Either portfolioId or both entityType and entityId must be provided, but not both forms. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "entityType", + "in": "query", + "description": "Type of the selection whose projects are returned as a flattened list. Must be provided together with entityId and cannot be combined with portfolioId. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "entityId", + "in": "query", + "description": "Portfolio ID or application branch ID, according to entityType. Must be provided together with entityType and cannot be combined with portfolioId. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "referenceDate", + "in": "query", + "description": "The date from which to return reference measure values for computing trends. If omitted, reference values will not be returned in the response. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + { + "name": "sort", + "in": "query", + "description": "Defines the sort order of project measures. The value is a comma-separated list of fields to sort by. Each field is prefixed with '+' (URL encoded) for ascending or '-' for descending. Valid sort fields are 'measure.currentValue' and 'projectName'. Sorting is done in lexicographical order, i.e. subsequent sort fields are used as tie breakers. ", + "required": false, + "schema": { + "type": "array", + "default": [ + "-measure.currentValue" + ], + "items": { + "type": "string" + } + } + }, + { + "name": "requireValue", + "in": "query", + "description": "Only return projects with a non-null metric value.", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Successfully got project measure values", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectMeasuresResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "TokenAuth": [] + } + ] + } + }, + "/history/project-issue-counts": { + "get": { + "tags": [ + "project-issue-counts" + ], + "summary": "Get project issue counts.", + "description": "Get issue counts for the flattened list of projects in a portfolio or application. Identify the selection with either the legacy portfolioId parameter, or with both entityType and entityId. The two selector forms are mutually exclusive. ", + "operationId": "getProjectIssueCounts", + "parameters": [ + { + "name": "portfolioId", + "in": "query", + "description": "Legacy portfolio ID. Either portfolioId or both entityType and entityId must be provided, but not both forms. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "entityType", + "in": "query", + "description": "Type of the selection whose projects are returned as a flattened list. Must be provided together with entityId and cannot be combined with portfolioId. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "entityId", + "in": "query", + "description": "Portfolio ID or application branch ID, according to entityType. Must be provided together with entityType and cannot be combined with portfolioId. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "ruleKeys", + "in": "query", + "description": "Comma separated list of ruleKeys", + "required": false, + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + { + "name": "severities", + "in": "query", + "description": "Limit results to issues with specific severities (comma-separated).", + "required": false, + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "enum": [ + "BLOCKER", + "HIGH", + "INFO", + "LOW", + "MEDIUM" + ] + } + } + }, + { + "name": "issueTypes", + "in": "query", + "description": "Limit results to issues with specific types (comma-separated).", + "required": false, + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "enum": [ + "BUG", + "CODE_SMELL", + "SECURITY_HOTSPOT", + "VULNERABILITY" + ] + } + } + }, + { + "name": "statuses", + "in": "query", + "description": "Limit results to issues with specific statuses (comma-separated). Reviewed security hotspots also support SAFE and FIXED.", + "required": false, + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "enum": [ + "ACCEPTED", + "CONFIRMED", + "FALSE_POSITIVE", + "FIXED", + "OPEN", + "REVIEWED", + "SAFE", + "TO_REVIEW" + ] + } + } + }, + { + "name": "impacts", + "in": "query", + "description": "Limit results to issues with specific impacts (comma-separated). An impact is a combination of software quality and severity. ", + "required": false, + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + { + "name": "nameContains", + "in": "query", + "description": "Search query for project names, case insensitive", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "referenceDate", + "in": "query", + "description": "The date from which to return reference measure values for computing trends. If omitted, reference values will not be returned in the response. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + { + "name": "pageIndex", + "in": "query", + "description": "Index of the page to fetch", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 1, + "minimum": 1 + } + }, + { + "name": "pageSize", + "in": "query", + "description": "Size of the page to fetch.", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50, + "minimum": 0 + } + }, + { + "name": "sort", + "in": "query", + "description": "Defines the sort order of project issue counts. The value is a comma-separated list of fields to sort by. Each field is prefixed with '+' (URL encoded) for ascending or '-' for descending. Valid sort fields are 'issueCount' and 'projectName'. Sorting is done in lexicographical order, i.e. subsequent sort fields are used as tie breakers. ", + "required": false, + "schema": { + "type": "array", + "default": [ + "-issueCount" + ], + "items": { + "type": "string" + } + } + }, + { + "name": "requireIssues", + "in": "query", + "description": "Only return projects with an issue count greater than 0.", + "required": false, + "schema": { + "type": "boolean", + "default": false + } + } + ], + "responses": { + "200": { + "description": "Successfully got project issue counts", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectIssueCountsResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "TokenAuth": [] + } + ] + } + }, + "/history/measures-history": { + "get": { + "tags": [ + "measures-history" + ], + "summary": "Get measures history.", + "description": "Returns the history of measures for the given entity.", + "operationId": "getMeasuresHistory", + "parameters": [ + { + "name": "entityType", + "in": "query", + "description": "Type of entity to retrieve measures history for", + "required": true, + "schema": { + "type": "string", + "enum": [ + "PORTFOLIO", + "PROJECT_BRANCH" + ] + } + }, + { + "name": "entityId", + "in": "query", + "description": "UUID of the entity", + "required": true, + "schema": { + "type": "string", + "maxLength": 36, + "minLength": 36 + } + }, + { + "name": "metricKeys", + "in": "query", + "description": "Comma-separated list of metric keys to filter by", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "startDate", + "in": "query", + "description": "Inclusive start of the date range (ISO-8601)", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "endDate", + "in": "query", + "description": "Inclusive end of the date range (ISO-8601). Defaults to current date.", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "Measures history for the given entity", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MeasuresHistoryResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + }, + "security": [ + { + "TokenAuth": [] + } + ] + } + }, + "/history/issue-count-history": { + "get": { + "tags": [ + "issue-count-history" + ], + "summary": "Provides issue count history data for an entity", + "description": "Returns the history of issue counts for an entity. The maximum resolution for history data is 1 day. You cannot zoom in on a period less than 1 day (e.g. 1hour). All history is recorded in UTC timezone. Issue counts will carry-forward for days with no changes. ", + "operationId": "getIssueCountHistory", + "parameters": [ + { + "name": "entityId", + "in": "query", + "description": "The ID of the entity to retrieve issue count history for.", + "required": true, + "schema": { + "type": "string", + "maxLength": 36, + "minLength": 36 + } + }, + { + "name": "entityType", + "in": "query", + "description": "The entity's type.", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "startDate", + "in": "query", + "description": "The date and time to begin returning issue count history in ISO 8601 format (inclusive). The start date may not be more than 1 year in the past. ", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "endDate", + "in": "query", + "description": "The date and time to stop returning issue count history in ISO 8601 format (inclusive). If not specified, the current date and time at UTC offset 0 will be used. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ], + "format": "date-time" + } + }, + { + "name": "impacts", + "in": "query", + "description": "Limit results to issues with specific impacts (comma-separated). An impact is a combination of software quality and severity. ", + "required": false, + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + { + "name": "issueTypes", + "in": "query", + "description": "Limit results to issues with specific types (comma-separated).", + "required": false, + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "enum": [ + "BUG", + "CODE_SMELL", + "SECURITY_HOTSPOT", + "VULNERABILITY" + ] + } + } + }, + { + "name": "ruleKeys", + "in": "query", + "description": "Comma separated list of ruleKeys", + "required": false, + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + } + }, + { + "name": "severities", + "in": "query", + "description": "Limit results to issues with specific severities (comma-separated).", + "required": false, + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "enum": [ + "BLOCKER", + "HIGH", + "INFO", + "LOW", + "MEDIUM" + ] + } + } + }, + { + "name": "sliceBy", + "in": "query", + "description": "Optionally group issue counts by a specific dimension. If not provided, issue counts will contain a single distribution with the count of all issues. ", + "required": false, + "schema": { + "type": [ + "string", + "null" + ] + } + }, + { + "name": "statuses", + "in": "query", + "description": "Limit results to issues with specific statuses (comma-separated). Reviewed security hotspots also support SAFE and FIXED.", + "required": false, + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "enum": [ + "ACCEPTED", + "CONFIRMED", + "FALSE_POSITIVE", + "FIXED", + "OPEN", + "REVIEWED", + "SAFE", + "TO_REVIEW" + ] + } + } + } + ], + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IssueCountHistoryResponse" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "429": { + "description": "Too Many Requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } } - } - ], - "responses": { - "200": { - "description": "OK", + }, + "500": { + "description": "Internal Server Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Health" + "$ref": "#/components/schemas/ErrorResponse" } } } } - } + }, + "security": [ + { + "TokenAuth": [] + } + ] } }, "/dop-translation/project-bindings": { @@ -2282,6 +3244,17 @@ } } }, + "GithubInstallationTokenRestResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "expiresAt": { + "type": "string" + } + } + }, "GithubConfigurationCreateRestRequest": { "type": "object", "properties": { @@ -3269,6 +4242,305 @@ } } }, + "Pagination": { + "type": "object", + "properties": { + "pageIndex": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "pageSize": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "total": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + } + }, + "ProjectMeasure": { + "type": "object", + "properties": { + "branchId": { + "type": "string" + }, + "branchName": { + "type": [ + "string", + "null" + ], + "example": "develop" + }, + "measure": { + "$ref": "#/components/schemas/ProjectMeasureMetric" + }, + "projectKey": { + "type": "string", + "example": "my-project" + }, + "projectName": { + "type": "string", + "example": "My Project" + } + }, + "required": [ + "branchId", + "measure", + "projectKey", + "projectName" + ] + }, + "ProjectMeasureMetric": { + "type": "object", + "properties": { + "currentValue": { + "type": [ + "string", + "null" + ], + "description": "Value of the measure encoded as a string. Use the measure's type to interpret the value. The value may be null if the measure has no current value. " + }, + "metric": { + "type": "string", + "description": "Metric key", + "example": "coverage" + }, + "referenceValue": { + "type": [ + "string", + "null" + ], + "description": "Reference value for the measure encoded as a string, used for trend calculation. Use the measure's type to interpret the value. The value may be null if the measure did not have a value on the reference date. " + }, + "type": { + "type": "string", + "description": "Identifies the measure's data type.", + "example": "PERCENT" + } + }, + "required": [ + "metric", + "type" + ] + }, + "ProjectMeasuresResponse": { + "type": "object", + "properties": { + "hiddenProjectCount": { + "type": "integer", + "format": "int32", + "description": "The number of projects excluded from the response because the user does not have permission to view those projects. " + }, + "projectMeasures": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectMeasure" + } + }, + "page": { + "$ref": "#/components/schemas/Pagination" + } + }, + "required": [ + "hiddenProjectCount", + "page", + "projectMeasures" + ] + }, + "ErrorResponse": { + "type": "object", + "properties": { + "message": { + "type": [ + "string", + "null" + ], + "description": "Error message" + } + } + }, + "ProjectIssueCount": { + "type": "object", + "properties": { + "branchId": { + "type": "string" + }, + "projectName": { + "type": "string", + "example": "My Project" + }, + "projectKey": { + "type": "string", + "example": "my-project" + }, + "branchName": { + "type": [ + "string", + "null" + ], + "example": "develop" + }, + "issueCount": { + "type": "integer", + "format": "int64" + }, + "referenceIssueCount": { + "type": [ + "integer", + "null" + ], + "format": "int64" + } + }, + "required": [ + "branchId", + "issueCount", + "projectKey", + "projectName" + ] + }, + "ProjectIssueCountsResponse": { + "type": "object", + "properties": { + "hiddenProjectCount": { + "type": "integer", + "format": "int32", + "description": "The number of projects excluded from the response because the user does not have permission to view those projects. " + }, + "projectIssueCounts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProjectIssueCount" + } + }, + "page": { + "$ref": "#/components/schemas/Pagination" + } + }, + "required": [ + "hiddenProjectCount", + "page", + "projectIssueCounts" + ] + }, + "MeasureHistoryEntry": { + "type": "object", + "properties": { + "metric": { + "type": [ + "string", + "null" + ], + "description": "Metric whose value is being recorded", + "example": "coverage" + }, + "type": { + "type": [ + "string", + "null" + ], + "description": "Type of value the measure contains", + "example": "PERCENT" + }, + "value": { + "type": [ + "string", + "null" + ], + "description": "Value of the measure", + "example": "81.2" + } + } + }, + "MeasureHistoryItem": { + "type": "object", + "properties": { + "date": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "The date the measures were taken" + }, + "measures": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MeasureHistoryEntry" + } + } + } + }, + "MeasuresHistoryResponse": { + "type": "object", + "properties": { + "measuresHistory": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MeasureHistoryItem" + } + } + } + }, + "IssueCountDistribution": { + "type": "object", + "properties": { + "key": { + "type": [ + "string", + "null" + ] + }, + "value": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "IssueCountHistory": { + "type": "object", + "properties": { + "date": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Date and time in ISO 8601 format, including timezone offset", + "example": "2024-01-01T12:00Z" + }, + "distribution": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IssueCountDistribution" + } + } + } + }, + "IssueCountHistoryResponse": { + "type": "object", + "properties": { + "issueCountHistory": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IssueCountHistory" + } + } + } + }, "ProjectBinding": { "type": "object", "properties": { diff --git a/integration_testing/v2_dop_translation_test.go b/integration_testing/v2_dop_translation_test.go index a7532bc..7cdd0cb 100644 --- a/integration_testing/v2_dop_translation_test.go +++ b/integration_testing/v2_dop_translation_test.go @@ -243,4 +243,50 @@ var _ = Describe("V2 DOP Translation Service", Ordered, func() { }) }) }) + + // ========================================================================= + // GenerateInstallationToken + // ========================================================================= + Describe("GenerateInstallationToken", func() { + Context("parameter validation", func() { + It("should fail with nil options", func() { + result, resp, err := client.V2.DopTranslation.GenerateInstallationToken(context.Background(), nil) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + + It("should fail without a project key", func() { + result, resp, err := client.V2.DopTranslation.GenerateInstallationToken(context.Background(), &sonar.DopTranslationGenerateInstallationTokenOptions{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Project")) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + }) + + Context("functional", func() { + It("should mint a token or return an expected error", func() { + // No GitHub App is configured on a vanilla instance, and the + // project is not bound, so an error response is expected. Only + // accept the documented failure codes so unrelated failures are + // not silently swallowed. + result, resp, err := client.V2.DopTranslation.GenerateInstallationToken(context.Background(), &sonar.DopTranslationGenerateInstallationTokenOptions{ + Project: helpers.UniqueResourceName("v2dopproj"), + }) + if err != nil { + Expect(resp).NotTo(BeNil()) + Expect(resp.StatusCode).To(BeElementOf( + http.StatusBadRequest, + http.StatusForbidden, + http.StatusNotFound, + http.StatusInternalServerError, + )) + } else { + Expect(resp.StatusCode).To(BeNumerically("<", 400)) + Expect(result).NotTo(BeNil()) + } + }) + }) + }) }) diff --git a/integration_testing/v2_history_test.go b/integration_testing/v2_history_test.go new file mode 100644 index 0000000..51fe4b0 --- /dev/null +++ b/integration_testing/v2_history_test.go @@ -0,0 +1,218 @@ +package integration_testing_test + +import ( + "context" + "net/http" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/boxboxjason/sonarqube-client-go/v2/integration_testing/helpers" + "github.com/boxboxjason/sonarqube-client-go/v2/sonar" +) + +var _ = Describe("V2 History Service", Ordered, func() { + var client *sonar.Client + + // A syntactically valid (but non-existent) 36-character entity UUID. + const dummyEntityID = "00000000-0000-0000-0000-000000000000" + + startDate := time.Now().AddDate(0, -1, 0).UTC().Format(time.RFC3339) + + // expectedErrorCodes are the status codes an unlicensed community instance + // may return for these portfolio/application-scoped endpoints. Listing them + // explicitly keeps unrelated failures (decode errors, panics) from being + // silently swallowed. + expectedErrorCodes := []int{ + http.StatusBadRequest, + http.StatusForbidden, + http.StatusNotFound, + http.StatusPaymentRequired, + } + + // entityHistoryErrorCodes additionally tolerates HTTP 500: on the community + // edition the measures/issue-count history endpoints return a generic + // server error (rather than 400/404) when the referenced entity UUID does + // not resolve, because the backing portfolio/governance features are not + // available in this edition. + entityHistoryErrorCodes := append([]int{http.StatusInternalServerError}, expectedErrorCodes...) + + BeforeAll(func() { + var err error + client, err = helpers.NewDefaultClient() + Expect(err).NotTo(HaveOccurred()) + Expect(client).NotTo(BeNil()) + }) + + // ========================================================================= + // GetIssueCountHistory + // ========================================================================= + Describe("GetIssueCountHistory", func() { + Context("parameter validation", func() { + It("should fail with nil options", func() { + result, resp, err := client.V2.History.GetIssueCountHistory(context.Background(), nil) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + + It("should fail without required fields", func() { + result, resp, err := client.V2.History.GetIssueCountHistory(context.Background(), &sonar.HistoryIssueCountHistoryOptions{ + EntityType: "PROJECT_BRANCH", + }) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + }) + + Context("functional", func() { + It("should return issue count history or an expected error", func() { + result, resp, err := client.V2.History.GetIssueCountHistory(context.Background(), &sonar.HistoryIssueCountHistoryOptions{ + EntityID: dummyEntityID, + EntityType: "PROJECT_BRANCH", + StartDate: startDate, + }) + if err != nil { + Expect(resp).NotTo(BeNil()) + Expect(resp.StatusCode).To(BeElementOf(entityHistoryErrorCodes)) + } else { + Expect(resp.StatusCode).To(BeNumerically("<", 400)) + Expect(result).NotTo(BeNil()) + } + }) + }) + }) + + // ========================================================================= + // GetMeasuresHistory + // ========================================================================= + Describe("GetMeasuresHistory", func() { + Context("parameter validation", func() { + It("should fail with nil options", func() { + result, resp, err := client.V2.History.GetMeasuresHistory(context.Background(), nil) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + + It("should fail with an invalid entity type", func() { + result, resp, err := client.V2.History.GetMeasuresHistory(context.Background(), &sonar.HistoryMeasuresHistoryOptions{ + EntityType: "BRANCH", + EntityID: dummyEntityID, + MetricKeys: []string{"coverage"}, + StartDate: startDate, + }) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + }) + + Context("functional", func() { + It("should return measures history or an expected error", func() { + result, resp, err := client.V2.History.GetMeasuresHistory(context.Background(), &sonar.HistoryMeasuresHistoryOptions{ + EntityType: sonar.HistoryEntityTypeProjectBranch, + EntityID: dummyEntityID, + MetricKeys: []string{"coverage", "bugs"}, + StartDate: startDate, + }) + if err != nil { + Expect(resp).NotTo(BeNil()) + Expect(resp.StatusCode).To(BeElementOf(entityHistoryErrorCodes)) + } else { + Expect(resp.StatusCode).To(BeNumerically("<", 400)) + Expect(result).NotTo(BeNil()) + } + }) + }) + }) + + // ========================================================================= + // GetProjectIssueCounts + // ========================================================================= + Describe("GetProjectIssueCounts", func() { + Context("parameter validation", func() { + It("should fail with nil options", func() { + result, resp, err := client.V2.History.GetProjectIssueCounts(context.Background(), nil) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + + It("should fail when no selector is provided", func() { + result, resp, err := client.V2.History.GetProjectIssueCounts(context.Background(), &sonar.HistoryProjectIssueCountsOptions{}) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + + It("should fail when both selector forms are provided", func() { + result, resp, err := client.V2.History.GetProjectIssueCounts(context.Background(), &sonar.HistoryProjectIssueCountsOptions{ + PortfolioID: "portfolio-1", + EntityType: "PORTFOLIO", + EntityID: dummyEntityID, + }) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + }) + + Context("functional", func() { + It("should return project issue counts or an expected error", func() { + result, resp, err := client.V2.History.GetProjectIssueCounts(context.Background(), &sonar.HistoryProjectIssueCountsOptions{ + PortfolioID: helpers.UniqueResourceName("portfolio"), + }) + if err != nil { + Expect(resp).NotTo(BeNil()) + Expect(resp.StatusCode).To(BeElementOf(entityHistoryErrorCodes)) + } else { + Expect(resp.StatusCode).To(BeNumerically("<", 400)) + Expect(result).NotTo(BeNil()) + } + }) + }) + }) + + // ========================================================================= + // GetProjectMeasures + // ========================================================================= + Describe("GetProjectMeasures", func() { + Context("parameter validation", func() { + It("should fail with nil options", func() { + result, resp, err := client.V2.History.GetProjectMeasures(context.Background(), nil) + Expect(err).To(HaveOccurred()) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + + It("should fail without a metric key", func() { + result, resp, err := client.V2.History.GetProjectMeasures(context.Background(), &sonar.HistoryProjectMeasuresOptions{ + PortfolioID: "portfolio-1", + }) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("MetricKey")) + Expect(resp).To(BeNil()) + Expect(result).To(BeNil()) + }) + }) + + Context("functional", func() { + It("should return project measures or an expected error", func() { + result, resp, err := client.V2.History.GetProjectMeasures(context.Background(), &sonar.HistoryProjectMeasuresOptions{ + MetricKey: "coverage", + PortfolioID: helpers.UniqueResourceName("portfolio"), + }) + if err != nil { + Expect(resp).NotTo(BeNil()) + Expect(resp.StatusCode).To(BeElementOf(entityHistoryErrorCodes)) + } else { + Expect(resp.StatusCode).To(BeNumerically("<", 400)) + Expect(result).NotTo(BeNil()) + } + }) + }) + }) +}) diff --git a/sonar/client.go b/sonar/client.go index 56070c3..0956f3a 100644 --- a/sonar/client.go +++ b/sonar/client.go @@ -131,6 +131,8 @@ type ServicesV2 struct { DopTranslation *DopTranslationService // FixSuggestions provides methods for the Fix Suggestions V2 API. FixSuggestions *FixSuggestionsService + // History provides methods for the History V2 API. + History *HistoryService // Marketplace provides methods for the Marketplace V2 API. Marketplace *MarketplaceService // Sca provides methods for the SCA V2 API. @@ -598,6 +600,7 @@ func initServicesV2(client *Client) { DopTranslation: &DopTranslationService{client: client}, Entitlements: &EntitlementsService{client: client}, FixSuggestions: &FixSuggestionsService{client: client}, + History: &HistoryService{client: client}, Integrations: &IntegrationsService{client: client}, Issues: &IssuesV2Service{client: client}, Jira: &JiraService{client: client}, diff --git a/sonar/dop_translation_v2_service.go b/sonar/dop_translation_v2_service.go index 6ea5e84..a2d45ea 100644 --- a/sonar/dop_translation_v2_service.go +++ b/sonar/dop_translation_v2_service.go @@ -62,6 +62,15 @@ type DopTranslationDopSettings struct { // rather than a fixed struct. type DopTranslationJfrogEvidence map[string]any +// DopTranslationGithubInstallationToken represents a minted, short-lived GitHub +// App installation token scoped to the bound repository of a project. +type DopTranslationGithubInstallationToken struct { + // Token is the short-lived GitHub App installation token. + Token string `json:"token,omitempty"` + // ExpiresAt is the token expiration timestamp. + ExpiresAt string `json:"expiresAt,omitempty"` +} + // ----------------------------------------------------------------------------- // Request Types // ----------------------------------------------------------------------------- @@ -95,6 +104,14 @@ type DopTranslationBoundProjectOptions struct { RepositoryIdentifier string `json:"repositoryIdentifier"` } +// DopTranslationGenerateInstallationTokenOptions contains parameters for minting +// a GitHub App installation token. +type DopTranslationGenerateInstallationTokenOptions struct { + // Project is the key of the project whose bound repository the token is + // scoped to. This field is required. + Project string `json:"project"` +} + // ----------------------------------------------------------------------------- // Validation // ----------------------------------------------------------------------------- @@ -128,6 +145,16 @@ func (s *DopTranslationService) ValidateCreateBoundProjectRequest(opt *DopTransl return nil } +// ValidateGenerateInstallationTokenRequest validates the +// DopTranslationGenerateInstallationTokenOptions. +func (s *DopTranslationService) ValidateGenerateInstallationTokenRequest(opt *DopTranslationGenerateInstallationTokenOptions) error { + if opt == nil { + return NewValidationError("opt", "must not be nil", ErrMissingRequired) + } + + return ValidateRequired(opt.Project, "Project") +} + // ----------------------------------------------------------------------------- // Service Methods // ----------------------------------------------------------------------------- @@ -225,3 +252,32 @@ func (s *DopTranslationService) GetJfrogEvidence(ctx context.Context, taskID str return result, resp, nil } + +// GenerateInstallationToken mints a short-lived GitHub App installation token, +// scoped to the bound repository of the given project. +// Requires the 'Administer System' permission. +// +// This endpoint is marked internal by SonarQube (x-sonar-internal) and its +// request/response contract may change without notice between SonarQube versions. +// +// API endpoint: POST /api/v2/dop-translation/github-installation-tokens. +func (s *DopTranslationService) GenerateInstallationToken(ctx context.Context, opt *DopTranslationGenerateInstallationTokenOptions) (*DopTranslationGithubInstallationToken, *http.Response, error) { + err := s.ValidateGenerateInstallationTokenRequest(opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(ctx, http.MethodPost, "dop-translation/github-installation-tokens", opt, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(DopTranslationGithubInstallationToken) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} diff --git a/sonar/dop_translation_v2_service_test.go b/sonar/dop_translation_v2_service_test.go index e147a07..1d3bfb7 100644 --- a/sonar/dop_translation_v2_service_test.go +++ b/sonar/dop_translation_v2_service_test.go @@ -155,3 +155,47 @@ func TestDopTranslationV2_GetJfrogEvidence_ValidationError(t *testing.T) { assert.Nil(t, result) assert.Nil(t, resp) } + +// ============================================================================= +// GenerateInstallationToken +// ============================================================================= + +func TestDopTranslationV2_GenerateInstallationToken(t *testing.T) { + response := DopTranslationGithubInstallationToken{ + Token: "ghs_exampletoken", + ExpiresAt: "2026-09-10T12:00:00Z", + } + server := newTestServer(t, mockHandlerWithParams(t, http.MethodPost, "/v2/dop-translation/github-installation-tokens", http.StatusOK, + map[string]string{"project": "my-project"}, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.DopTranslation.GenerateInstallationToken(context.Background(), &DopTranslationGenerateInstallationTokenOptions{ + Project: "my-project", + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + require.NotNil(t, result) + assert.Equal(t, "ghs_exampletoken", result.Token) + assert.Equal(t, "2026-09-10T12:00:00Z", result.ExpiresAt) +} + +func TestDopTranslationV2_GenerateInstallationToken_ValidationError(t *testing.T) { + client := newLocalhostClient(t) + + tests := []struct { + opt *DopTranslationGenerateInstallationTokenOptions + name string + }{ + {nil, "nil opt"}, + {&DopTranslationGenerateInstallationTokenOptions{}, "missing Project"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, resp, err := client.V2.DopTranslation.GenerateInstallationToken(context.Background(), tt.opt) + assert.Error(t, err) + assert.Nil(t, result) + assert.Nil(t, resp) + }) + } +} diff --git a/sonar/history_v2_service.go b/sonar/history_v2_service.go new file mode 100644 index 0000000..a4090a4 --- /dev/null +++ b/sonar/history_v2_service.go @@ -0,0 +1,488 @@ +package sonar + +import ( + "context" + "fmt" + "net/http" +) + +const ( + // HistoryEntityTypePortfolio identifies a portfolio entity. + HistoryEntityTypePortfolio = "PORTFOLIO" + // HistoryEntityTypeProjectBranch identifies a project branch entity. + HistoryEntityTypeProjectBranch = "PROJECT_BRANCH" +) + +// HistoryService handles communication with the history related methods of the +// SonarQube V2 API (the "/history" endpoints: measures history, issue count +// history, and the flattened per-project measure and issue count listings for a +// portfolio or application). +type HistoryService struct { + // client is used to communicate with the SonarQube API. + client *Client +} + +//nolint:gochecknoglobals // constant set of allowed values +var allowedHistoryMeasuresEntityTypes = map[string]struct{}{ + HistoryEntityTypePortfolio: {}, + HistoryEntityTypeProjectBranch: {}, +} + +// ----------------------------------------------------------------------------- +// Response Types +// ----------------------------------------------------------------------------- + +// HistoryIssueCountDistribution represents a single bucket of an issue count +// distribution. +type HistoryIssueCountDistribution struct { + // Key is the distribution bucket key. It is empty when the counts are not + // grouped by any dimension. + Key string `json:"key,omitempty"` + // Value is the number of issues in the bucket. + Value int32 `json:"value,omitempty"` +} + +// HistoryIssueCount represents the issue count distribution recorded at a given +// point in time. +type HistoryIssueCount struct { + // Date is the date and time in ISO 8601 format, including timezone offset. + Date string `json:"date,omitempty"` + // Distribution is the list of issue count buckets for the date. + Distribution []HistoryIssueCountDistribution `json:"distribution,omitempty"` +} + +// HistoryIssueCountHistoryResponse represents the response from GetIssueCountHistory. +type HistoryIssueCountHistoryResponse struct { + // IssueCountHistory is the ordered list of issue count snapshots. + IssueCountHistory []HistoryIssueCount `json:"issueCountHistory,omitempty"` +} + +// HistoryMeasureEntry represents a single measure value inside a measures +// history item. +type HistoryMeasureEntry struct { + // Metric is the metric whose value is being recorded (e.g. "coverage"). + Metric string `json:"metric,omitempty"` + // Type is the type of value the measure contains (e.g. "PERCENT"). + Type string `json:"type,omitempty"` + // Value is the value of the measure encoded as a string. + Value string `json:"value,omitempty"` +} + +// HistoryMeasureItem represents the measures recorded at a given point in time. +type HistoryMeasureItem struct { + // Date is the date the measures were taken. + Date string `json:"date,omitempty"` + // Measures is the list of measure values for the date. + Measures []HistoryMeasureEntry `json:"measures,omitempty"` +} + +// HistoryMeasuresHistoryResponse represents the response from GetMeasuresHistory. +type HistoryMeasuresHistoryResponse struct { + // MeasuresHistory is the ordered list of measure snapshots. + MeasuresHistory []HistoryMeasureItem `json:"measuresHistory,omitempty"` +} + +// HistoryProjectIssueCount represents the issue count for a single project in a +// flattened portfolio or application listing. +// +//nolint:govet // Field alignment less important than maintaining consistent field order for readability +type HistoryProjectIssueCount struct { + // BranchID is the identifier of the project branch. + BranchID string `json:"branchId,omitempty"` + // ProjectName is the display name of the project. + ProjectName string `json:"projectName,omitempty"` + // ProjectKey is the key of the project. + ProjectKey string `json:"projectKey,omitempty"` + // BranchName is the name of the branch. + BranchName string `json:"branchName,omitempty"` + // IssueCount is the number of issues in the project branch. + IssueCount int64 `json:"issueCount,omitempty"` + // ReferenceIssueCount is the issue count at the reference date, used for + // trend computation. It is nil when no reference date was supplied. + ReferenceIssueCount *int64 `json:"referenceIssueCount,omitempty"` +} + +// HistoryProjectIssueCountsResponse represents the response from GetProjectIssueCounts. +// +//nolint:govet // Field alignment less important than maintaining consistent field order for readability +type HistoryProjectIssueCountsResponse struct { + // HiddenProjectCount is the number of projects excluded from the response + // because the user does not have permission to view them. + HiddenProjectCount int32 `json:"hiddenProjectCount,omitempty"` + // ProjectIssueCounts is the flattened list of per-project issue counts. + ProjectIssueCounts []HistoryProjectIssueCount `json:"projectIssueCounts,omitempty"` + // Page contains pagination information. + Page PageResponseV2 `json:"page,omitzero"` +} + +// HistoryProjectMeasureMetric represents the current and reference values of a +// single metric for a project. +type HistoryProjectMeasureMetric struct { + // CurrentValue is the current value of the measure encoded as a string. It + // is empty when the measure has no current value. + CurrentValue string `json:"currentValue,omitempty"` + // Metric is the metric key (e.g. "coverage"). + Metric string `json:"metric,omitempty"` + // ReferenceValue is the value of the measure at the reference date, used for + // trend computation. It is empty when there was no value on that date. + ReferenceValue string `json:"referenceValue,omitempty"` + // Type identifies the measure's data type (e.g. "PERCENT"). + Type string `json:"type,omitempty"` +} + +// HistoryProjectMeasure represents the measure value for a single project in a +// flattened portfolio or application listing. +type HistoryProjectMeasure struct { + // BranchID is the identifier of the project branch. + BranchID string `json:"branchId,omitempty"` + // BranchName is the name of the branch. + BranchName string `json:"branchName,omitempty"` + // Measure is the metric value for the project. + Measure HistoryProjectMeasureMetric `json:"measure,omitzero"` + // ProjectKey is the key of the project. + ProjectKey string `json:"projectKey,omitempty"` + // ProjectName is the display name of the project. + ProjectName string `json:"projectName,omitempty"` +} + +// HistoryProjectMeasuresResponse represents the response from GetProjectMeasures. +// +//nolint:govet // Field alignment less important than maintaining consistent field order for readability +type HistoryProjectMeasuresResponse struct { + // HiddenProjectCount is the number of projects excluded from the response + // because the user does not have permission to view them. + HiddenProjectCount int32 `json:"hiddenProjectCount,omitempty"` + // ProjectMeasures is the flattened list of per-project measure values. + ProjectMeasures []HistoryProjectMeasure `json:"projectMeasures,omitempty"` + // Page contains pagination information. + Page PageResponseV2 `json:"page,omitzero"` +} + +// ----------------------------------------------------------------------------- +// Option Types +// ----------------------------------------------------------------------------- + +// HistoryIssueCountHistoryOptions contains parameters for GetIssueCountHistory. +type HistoryIssueCountHistoryOptions struct { + // EntityID is the UUID of the entity to retrieve issue count history for. + // This field is required. + EntityID string `json:"entityId"` + // EntityType is the entity's type. This field is required. + EntityType string `json:"entityType"` + // StartDate is the inclusive start of the date range in ISO 8601 format. + // This field is required and may not be more than 1 year in the past. + StartDate string `json:"startDate"` + // EndDate is the inclusive end of the date range in ISO 8601 format. + // Defaults to the current date and time (UTC) when omitted. + EndDate string `json:"endDate,omitempty"` + // Impacts limits results to issues with specific impacts. + Impacts []string `json:"impacts,omitempty"` + // IssueTypes limits results to issues with specific types. + IssueTypes []string `json:"issueTypes,omitempty"` + // RuleKeys limits results to issues raised by specific rules. + RuleKeys []string `json:"ruleKeys,omitempty"` + // Severities limits results to issues with specific severities. + Severities []string `json:"severities,omitempty"` + // SliceBy optionally groups issue counts by a specific dimension. + SliceBy string `json:"sliceBy,omitempty"` + // Statuses limits results to issues with specific statuses. + Statuses []string `json:"statuses,omitempty"` +} + +// HistoryMeasuresHistoryOptions contains parameters for GetMeasuresHistory. +// +//nolint:govet // Field alignment less important than maintaining consistent field order for readability +type HistoryMeasuresHistoryOptions struct { + // EntityType is the type of entity to retrieve measures history for. + // This field is required. Allowed values: PORTFOLIO, PROJECT_BRANCH. + EntityType string `json:"entityType"` + // EntityID is the UUID of the entity. This field is required. + EntityID string `json:"entityId"` + // MetricKeys is the list of metric keys to filter by. This field is required. + MetricKeys []string `json:"metricKeys"` + // StartDate is the inclusive start of the date range in ISO 8601 format. + // This field is required. + StartDate string `json:"startDate"` + // EndDate is the inclusive end of the date range in ISO 8601 format. + // Defaults to the current date when omitted. + EndDate string `json:"endDate,omitempty"` +} + +// HistoryProjectIssueCountsOptions contains parameters for GetProjectIssueCounts. +// +// The selection is identified with either the legacy PortfolioID parameter, or +// with both EntityType and EntityID. The two selector forms are mutually +// exclusive. +// +//nolint:govet // Field alignment less important than maintaining consistent field order for readability +type HistoryProjectIssueCountsOptions struct { + // PortfolioID is the legacy portfolio ID. + PortfolioID string `json:"portfolioId,omitempty"` + // EntityType is the type of the selection whose projects are returned as a + // flattened list. Must be provided together with EntityID. + EntityType string `json:"entityType,omitempty"` + // EntityID is the portfolio ID or application branch ID, according to + // EntityType. Must be provided together with EntityType. + EntityID string `json:"entityId,omitempty"` + // RuleKeys limits results to issues raised by specific rules. + RuleKeys []string `json:"ruleKeys,omitempty"` + // Severities limits results to issues with specific severities. + Severities []string `json:"severities,omitempty"` + // IssueTypes limits results to issues with specific types. + IssueTypes []string `json:"issueTypes,omitempty"` + // Statuses limits results to issues with specific statuses. + Statuses []string `json:"statuses,omitempty"` + // Impacts limits results to issues with specific impacts. + Impacts []string `json:"impacts,omitempty"` + // NameContains is a case-insensitive search query for project names. + NameContains string `json:"nameContains,omitempty"` + // ReferenceDate is the date from which reference measure values are returned + // for computing trends. Reference values are omitted when this is empty. + ReferenceDate string `json:"referenceDate,omitempty"` + // PageIndex is the 1-based index of the page to fetch. Default is 1. + PageIndex int32 `json:"pageIndex,omitempty"` + // PageSize is the size of the page to fetch. Default is 50. + PageSize int32 `json:"pageSize,omitempty"` + // Sort defines the sort order. Each field is prefixed with '+' for ascending + // or '-' for descending. Valid fields: issueCount, projectName. + Sort []string `json:"sort,omitempty"` + // RequireIssues only returns projects with an issue count greater than 0. + RequireIssues bool `json:"requireIssues,omitempty"` +} + +// HistoryProjectMeasuresOptions contains parameters for GetProjectMeasures. +// +// The selection is identified with either the legacy PortfolioID parameter, or +// with both EntityType and EntityID. The two selector forms are mutually +// exclusive. +// +//nolint:govet // Field alignment less important than maintaining consistent field order for readability +type HistoryProjectMeasuresOptions struct { + // MetricKey is the metric key. This field is required. + MetricKey string `json:"metricKey"` + // MetricValue filters by exact metric value. Only primitive metric types are + // supported (not DISTR or DATA). + MetricValue string `json:"metricValue,omitempty"` + // NameContains is a case-insensitive search query for project names. + NameContains string `json:"nameContains,omitempty"` + // PageIndex is the 1-based index of the page to fetch. Default is 1. + PageIndex int32 `json:"pageIndex,omitempty"` + // PageSize is the size of the page to fetch. Default is 50. + PageSize int32 `json:"pageSize,omitempty"` + // PortfolioID is the legacy portfolio ID. + PortfolioID string `json:"portfolioId,omitempty"` + // EntityType is the type of the selection whose projects are returned as a + // flattened list. Must be provided together with EntityID. + EntityType string `json:"entityType,omitempty"` + // EntityID is the portfolio ID or application branch ID, according to + // EntityType. Must be provided together with EntityType. + EntityID string `json:"entityId,omitempty"` + // ReferenceDate is the date from which reference measure values are returned + // for computing trends. Reference values are omitted when this is empty. + ReferenceDate string `json:"referenceDate,omitempty"` + // Sort defines the sort order. Each field is prefixed with '+' for ascending + // or '-' for descending. Valid fields: measure.currentValue, projectName. + Sort []string `json:"sort,omitempty"` + // RequireValue only returns projects with a non-null metric value. + RequireValue bool `json:"requireValue,omitempty"` +} + +// ----------------------------------------------------------------------------- +// Validation Functions +// ----------------------------------------------------------------------------- + +// ValidateIssueCountHistoryOpt validates the options for GetIssueCountHistory. +func (s *HistoryService) ValidateIssueCountHistoryOpt(opt *HistoryIssueCountHistoryOptions) error { + if opt == nil { + return NewValidationError("opt", "option struct is required", ErrMissingRequired) + } + + err := ValidateRequired(opt.EntityID, "EntityID") + if err != nil { + return err + } + + err = ValidateRequired(opt.EntityType, "EntityType") + if err != nil { + return err + } + + return ValidateRequired(opt.StartDate, "StartDate") +} + +// ValidateMeasuresHistoryOpt validates the options for GetMeasuresHistory. +func (s *HistoryService) ValidateMeasuresHistoryOpt(opt *HistoryMeasuresHistoryOptions) error { + if opt == nil { + return NewValidationError("opt", "option struct is required", ErrMissingRequired) + } + + err := ValidateRequired(opt.EntityType, "EntityType") + if err != nil { + return err + } + + err = IsValueAuthorized(opt.EntityType, allowedHistoryMeasuresEntityTypes, "EntityType") + if err != nil { + return err + } + + err = ValidateRequired(opt.EntityID, "EntityID") + if err != nil { + return err + } + + if len(opt.MetricKeys) == 0 { + return NewValidationError("MetricKeys", "must not be empty", ErrMissingRequired) + } + + return ValidateRequired(opt.StartDate, "StartDate") +} + +// validateHistorySelector validates the mutually exclusive portfolioId / +// (entityType, entityId) selector shared by the project-scoped endpoints. +func validateHistorySelector(portfolioID, entityType, entityID string) error { + entityForm := entityType != "" || entityID != "" + + if portfolioID != "" && entityForm { + return NewValidationError("PortfolioID", "cannot be combined with EntityType/EntityID", ErrInvalidValue) + } + + if portfolioID == "" && !entityForm { + return NewValidationError("PortfolioID", "either PortfolioID or both EntityType and EntityID must be provided", ErrMissingRequired) + } + + if entityForm && (entityType == "" || entityID == "") { + return NewValidationError("EntityType", "EntityType and EntityID must be provided together", ErrMissingRequired) + } + + return nil +} + +// ValidateProjectIssueCountsOpt validates the options for GetProjectIssueCounts. +func (s *HistoryService) ValidateProjectIssueCountsOpt(opt *HistoryProjectIssueCountsOptions) error { + if opt == nil { + return NewValidationError("opt", "option struct is required", ErrMissingRequired) + } + + return validateHistorySelector(opt.PortfolioID, opt.EntityType, opt.EntityID) +} + +// ValidateProjectMeasuresOpt validates the options for GetProjectMeasures. +func (s *HistoryService) ValidateProjectMeasuresOpt(opt *HistoryProjectMeasuresOptions) error { + if opt == nil { + return NewValidationError("opt", "option struct is required", ErrMissingRequired) + } + + err := ValidateRequired(opt.MetricKey, "MetricKey") + if err != nil { + return err + } + + return validateHistorySelector(opt.PortfolioID, opt.EntityType, opt.EntityID) +} + +// ----------------------------------------------------------------------------- +// Service Methods +// ----------------------------------------------------------------------------- + +// GetIssueCountHistory returns the history of issue counts for an entity. The +// maximum resolution for history data is one day and all history is recorded in +// the UTC timezone. Issue counts carry forward for days with no changes. +// +// API endpoint: GET /api/v2/history/issue-count-history. +func (s *HistoryService) GetIssueCountHistory(ctx context.Context, opt *HistoryIssueCountHistoryOptions) (*HistoryIssueCountHistoryResponse, *http.Response, error) { + err := s.ValidateIssueCountHistoryOpt(opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(ctx, http.MethodGet, "history/issue-count-history", opt, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(HistoryIssueCountHistoryResponse) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// GetMeasuresHistory returns the history of measures for the given entity. +// +// API endpoint: GET /api/v2/history/measures-history. +func (s *HistoryService) GetMeasuresHistory(ctx context.Context, opt *HistoryMeasuresHistoryOptions) (*HistoryMeasuresHistoryResponse, *http.Response, error) { + err := s.ValidateMeasuresHistoryOpt(opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(ctx, http.MethodGet, "history/measures-history", opt, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(HistoryMeasuresHistoryResponse) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// GetProjectIssueCounts returns issue counts for the flattened list of projects +// in a portfolio or application. The selection is identified with either the +// legacy PortfolioID parameter, or with both EntityType and EntityID. +// +// API endpoint: GET /api/v2/history/project-issue-counts. +func (s *HistoryService) GetProjectIssueCounts(ctx context.Context, opt *HistoryProjectIssueCountsOptions) (*HistoryProjectIssueCountsResponse, *http.Response, error) { + err := s.ValidateProjectIssueCountsOpt(opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(ctx, http.MethodGet, "history/project-issue-counts", opt, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(HistoryProjectIssueCountsResponse) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} + +// GetProjectMeasures returns current measure values for the flattened list of +// projects in a portfolio or application. The selection is identified with +// either the legacy PortfolioID parameter, or with both EntityType and EntityID. +// +// API endpoint: GET /api/v2/history/project-measures. +func (s *HistoryService) GetProjectMeasures(ctx context.Context, opt *HistoryProjectMeasuresOptions) (*HistoryProjectMeasuresResponse, *http.Response, error) { + err := s.ValidateProjectMeasuresOpt(opt) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewSonarQubeV2APIRequest(ctx, http.MethodGet, "history/project-measures", opt, nil) + if err != nil { + return nil, nil, fmt.Errorf("failed to create request: %w", err) + } + + result := new(HistoryProjectMeasuresResponse) + + resp, err := s.client.Do(req, result) + if err != nil { + return nil, resp, err + } + + return result, resp, nil +} diff --git a/sonar/history_v2_service_test.go b/sonar/history_v2_service_test.go new file mode 100644 index 0000000..7ebaaf3 --- /dev/null +++ b/sonar/history_v2_service_test.go @@ -0,0 +1,274 @@ +package sonar + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ----------------------------------------------------------------------------- +// GetIssueCountHistory +// ----------------------------------------------------------------------------- + +func TestHistoryService_GetIssueCountHistory(t *testing.T) { + response := HistoryIssueCountHistoryResponse{ + IssueCountHistory: []HistoryIssueCount{ + { + Date: "2026-01-01T12:00:00Z", + Distribution: []HistoryIssueCountDistribution{ + {Key: "BUG", Value: 3}, + {Key: "CODE_SMELL", Value: 7}, + }, + }, + }, + } + server := newTestServer(t, mockHandlerWithParams(t, http.MethodGet, "/v2/history/issue-count-history", http.StatusOK, + map[string]string{ + "entityId": "00000000-0000-0000-0000-000000000000", + "entityType": "PROJECT_BRANCH", + "startDate": "2026-01-01T00:00:00Z", + "sliceBy": "issueType", + }, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.History.GetIssueCountHistory(context.Background(), &HistoryIssueCountHistoryOptions{ + EntityID: "00000000-0000-0000-0000-000000000000", + EntityType: "PROJECT_BRANCH", + StartDate: "2026-01-01T00:00:00Z", + SliceBy: "issueType", + IssueTypes: []string{"BUG", "CODE_SMELL"}, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + require.Len(t, result.IssueCountHistory, 1) + assert.Equal(t, "2026-01-01T12:00:00Z", result.IssueCountHistory[0].Date) + require.Len(t, result.IssueCountHistory[0].Distribution, 2) + assert.Equal(t, int32(3), result.IssueCountHistory[0].Distribution[0].Value) +} + +func TestHistoryService_GetIssueCountHistory_ValidationError(t *testing.T) { + client := newLocalhostClient(t) + svc := &HistoryService{client: client} + + tests := []struct { + opt *HistoryIssueCountHistoryOptions + name string + }{ + {nil, "nil opt"}, + {&HistoryIssueCountHistoryOptions{EntityType: "PROJECT_BRANCH", StartDate: "2026-01-01T00:00:00Z"}, "missing EntityID"}, + {&HistoryIssueCountHistoryOptions{EntityID: "id", StartDate: "2026-01-01T00:00:00Z"}, "missing EntityType"}, + {&HistoryIssueCountHistoryOptions{EntityID: "id", EntityType: "PROJECT_BRANCH"}, "missing StartDate"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, resp, err := svc.GetIssueCountHistory(context.Background(), tt.opt) + assert.Error(t, err) + assert.Nil(t, result) + assert.Nil(t, resp) + }) + } +} + +// ----------------------------------------------------------------------------- +// GetMeasuresHistory +// ----------------------------------------------------------------------------- + +func TestHistoryService_GetMeasuresHistory(t *testing.T) { + response := HistoryMeasuresHistoryResponse{ + MeasuresHistory: []HistoryMeasureItem{ + { + Date: "2026-01-01T12:00:00Z", + Measures: []HistoryMeasureEntry{ + {Metric: "coverage", Type: "PERCENT", Value: "81.2"}, + }, + }, + }, + } + server := newTestServer(t, mockHandlerWithParams(t, http.MethodGet, "/v2/history/measures-history", http.StatusOK, + map[string]string{ + "entityType": HistoryEntityTypeProjectBranch, + "entityId": "00000000-0000-0000-0000-000000000000", + "startDate": "2026-01-01T00:00:00Z", + }, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.History.GetMeasuresHistory(context.Background(), &HistoryMeasuresHistoryOptions{ + EntityType: HistoryEntityTypeProjectBranch, + EntityID: "00000000-0000-0000-0000-000000000000", + MetricKeys: []string{"coverage", "bugs"}, + StartDate: "2026-01-01T00:00:00Z", + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + require.Len(t, result.MeasuresHistory, 1) + require.Len(t, result.MeasuresHistory[0].Measures, 1) + assert.Equal(t, "coverage", result.MeasuresHistory[0].Measures[0].Metric) + assert.Equal(t, "81.2", result.MeasuresHistory[0].Measures[0].Value) +} + +func TestHistoryService_GetMeasuresHistory_ValidationError(t *testing.T) { + client := newLocalhostClient(t) + svc := &HistoryService{client: client} + + tests := []struct { + opt *HistoryMeasuresHistoryOptions + name string + }{ + {nil, "nil opt"}, + {&HistoryMeasuresHistoryOptions{EntityID: "id", MetricKeys: []string{"coverage"}, StartDate: "d"}, "missing EntityType"}, + {&HistoryMeasuresHistoryOptions{EntityType: "BRANCH", EntityID: "id", MetricKeys: []string{"coverage"}, StartDate: "d"}, "invalid EntityType"}, + {&HistoryMeasuresHistoryOptions{EntityType: HistoryEntityTypePortfolio, MetricKeys: []string{"coverage"}, StartDate: "d"}, "missing EntityID"}, + {&HistoryMeasuresHistoryOptions{EntityType: HistoryEntityTypePortfolio, EntityID: "id", StartDate: "d"}, "missing MetricKeys"}, + {&HistoryMeasuresHistoryOptions{EntityType: HistoryEntityTypePortfolio, EntityID: "id", MetricKeys: []string{"coverage"}}, "missing StartDate"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, resp, err := svc.GetMeasuresHistory(context.Background(), tt.opt) + assert.Error(t, err) + assert.Nil(t, result) + assert.Nil(t, resp) + }) + } +} + +// ----------------------------------------------------------------------------- +// GetProjectIssueCounts +// ----------------------------------------------------------------------------- + +func TestHistoryService_GetProjectIssueCounts(t *testing.T) { + refCount := int64(4) + response := HistoryProjectIssueCountsResponse{ + HiddenProjectCount: 2, + ProjectIssueCounts: []HistoryProjectIssueCount{ + { + BranchID: "branch-1", + ProjectName: "My Project", + ProjectKey: "my-project", + BranchName: "main", + IssueCount: 9, + ReferenceIssueCount: &refCount, + }, + }, + Page: PageResponseV2{PageIndex: 1, PageSize: 50, Total: 1}, + } + server := newTestServer(t, mockHandlerWithParams(t, http.MethodGet, "/v2/history/project-issue-counts", http.StatusOK, + map[string]string{ + "portfolioId": "portfolio-1", + "requireIssues": "true", + }, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.History.GetProjectIssueCounts(context.Background(), &HistoryProjectIssueCountsOptions{ + PortfolioID: "portfolio-1", + RequireIssues: true, + Sort: []string{"-issueCount"}, + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, int32(2), result.HiddenProjectCount) + require.Len(t, result.ProjectIssueCounts, 1) + assert.Equal(t, "my-project", result.ProjectIssueCounts[0].ProjectKey) + assert.Equal(t, int64(9), result.ProjectIssueCounts[0].IssueCount) + require.NotNil(t, result.ProjectIssueCounts[0].ReferenceIssueCount) + assert.Equal(t, int64(4), *result.ProjectIssueCounts[0].ReferenceIssueCount) + assert.Equal(t, int32(1), result.Page.Total) +} + +func TestHistoryService_GetProjectIssueCounts_ValidationError(t *testing.T) { + client := newLocalhostClient(t) + svc := &HistoryService{client: client} + + tests := []struct { + opt *HistoryProjectIssueCountsOptions + name string + }{ + {nil, "nil opt"}, + {&HistoryProjectIssueCountsOptions{}, "no selector"}, + {&HistoryProjectIssueCountsOptions{PortfolioID: "p", EntityType: "PORTFOLIO", EntityID: "e"}, "both selector forms"}, + {&HistoryProjectIssueCountsOptions{EntityType: "PORTFOLIO"}, "entityType without entityId"}, + {&HistoryProjectIssueCountsOptions{EntityID: "e"}, "entityId without entityType"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, resp, err := svc.GetProjectIssueCounts(context.Background(), tt.opt) + assert.Error(t, err) + assert.Nil(t, result) + assert.Nil(t, resp) + }) + } +} + +// ----------------------------------------------------------------------------- +// GetProjectMeasures +// ----------------------------------------------------------------------------- + +func TestHistoryService_GetProjectMeasures(t *testing.T) { + response := HistoryProjectMeasuresResponse{ + HiddenProjectCount: 0, + ProjectMeasures: []HistoryProjectMeasure{ + { + BranchID: "branch-1", + BranchName: "main", + ProjectKey: "my-project", + ProjectName: "My Project", + Measure: HistoryProjectMeasureMetric{ + CurrentValue: "81.2", + Metric: "coverage", + ReferenceValue: "79.0", + Type: "PERCENT", + }, + }, + }, + Page: PageResponseV2{PageIndex: 1, PageSize: 50, Total: 1}, + } + server := newTestServer(t, mockHandlerWithParams(t, http.MethodGet, "/v2/history/project-measures", http.StatusOK, + map[string]string{ + "metricKey": "coverage", + "entityType": "APPLICATION", + "entityId": "app-branch-1", + }, response)) + client := newTestClient(t, server.url()) + + result, resp, err := client.V2.History.GetProjectMeasures(context.Background(), &HistoryProjectMeasuresOptions{ + MetricKey: "coverage", + EntityType: "APPLICATION", + EntityID: "app-branch-1", + }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + require.Len(t, result.ProjectMeasures, 1) + assert.Equal(t, "my-project", result.ProjectMeasures[0].ProjectKey) + assert.Equal(t, "81.2", result.ProjectMeasures[0].Measure.CurrentValue) + assert.Equal(t, "PERCENT", result.ProjectMeasures[0].Measure.Type) +} + +func TestHistoryService_GetProjectMeasures_ValidationError(t *testing.T) { + client := newLocalhostClient(t) + svc := &HistoryService{client: client} + + tests := []struct { + opt *HistoryProjectMeasuresOptions + name string + }{ + {nil, "nil opt"}, + {&HistoryProjectMeasuresOptions{EntityType: "PORTFOLIO", EntityID: "e"}, "missing MetricKey"}, + {&HistoryProjectMeasuresOptions{MetricKey: "coverage"}, "no selector"}, + {&HistoryProjectMeasuresOptions{MetricKey: "coverage", PortfolioID: "p", EntityID: "e"}, "both selector forms"}, + {&HistoryProjectMeasuresOptions{MetricKey: "coverage", EntityType: "PORTFOLIO"}, "entityType without entityId"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, resp, err := svc.GetProjectMeasures(context.Background(), tt.opt) + assert.Error(t, err) + assert.Nil(t, result) + assert.Nil(t, resp) + }) + } +} From d8541010dc44b1ef3a13be369ef56bf17e89327b Mon Sep 17 00:00:00 2001 From: BoxBoxJason Date: Thu, 10 Sep 2026 22:56:19 +0200 Subject: [PATCH 3/3] style: correct lint error Signed-off-by: BoxBoxJason --- .golangci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.golangci.yml b/.golangci.yml index ee29e5d..9e96705 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -29,6 +29,7 @@ linters: - depguard - lll - err113 + - exhaustruct - exhaustruct_v5 # v2.13 rename of exhaustruct; keep the stable name so //nolint:exhaustruct directives still apply - gomodguard - modernize # stylistic-only suggestions (e.g. reflect.TypeAssert), too noisy for this codebase