Skip to content

Commit bd05ea9

Browse files
authored
Allow-housekeeper-to-override-keep-when-inactive-pattern (#2013)
* Fixes #2005 * Add doc for #2005 * Quality pass * Allow to select col to search by col number on top of col name * Add tests for #2005
1 parent ba5d08d commit bd05ea9

File tree

5 files changed

+59
-15
lines changed

5 files changed

+59
-15
lines changed

cli/housekeeper.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#!/usr/bin/env python3
21
#
32
# sonar-tools
43
# Copyright (C) 2019-2025 Olivier Korach
@@ -124,6 +123,12 @@ def _parse_arguments() -> object:
124123
default=_DEFAULT_BRANCH_OBSOLESCENCE,
125124
help=f"Deletes branches not to be kept and not analyzed since a given number of days, by default {_DEFAULT_BRANCH_OBSOLESCENCE} days",
126125
)
126+
parser.add_argument(
127+
"--keepWhenInactive",
128+
required=False,
129+
type=str,
130+
help="Regexp of branches to keep when inactive, overrides the SonarQube default sonar.dbcleaner.branchesToKeepWhenInactive value",
131+
)
127132
parser.add_argument(
128133
"-R",
129134
"--pullrequestsMaxAge",
@@ -195,12 +200,13 @@ def main() -> None:
195200
sq.verify_connection()
196201
sq.set_user_agent(f"{TOOL_NAME} {version.PACKAGE_VERSION}")
197202

198-
mode, proj_age, branch_age, pr_age, token_age = (
203+
mode, proj_age, branch_age, pr_age, token_age, keep_regexp = (
199204
kwargs["mode"],
200205
kwargs["projectsMaxAge"],
201206
kwargs["branchesMaxAge"],
202207
kwargs["pullrequestsMaxAge"],
203208
kwargs["tokensMaxAge"],
209+
kwargs.get("keepWhenInactive", None),
204210
)
205211
settings = {
206212
"audit.tokens.maxAge": token_age,
@@ -209,6 +215,7 @@ def main() -> None:
209215
PROJ_MAX_AGE: proj_age,
210216
"audit.projects.branches.maxLastAnalysisAge": branch_age,
211217
"audit.projects.pullRequests.maxLastAnalysisAge": pr_age,
218+
"audit.projects.branches.keepWhenInactive": keep_regexp,
212219
c.AUDIT_MODE_PARAM: "housekeeper",
213220
options.NBR_THREADS: kwargs[options.NBR_THREADS],
214221
}

doc/sonar-housekeeper.md

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,44 @@
1-
# <a name="sonar-housekeeper">
1+
# sonar-housekeeper
22

33
Deletes obsolete/outdated data from SonarQube:
44
- Projects whose last analysis date (on any branch) is older than a given number of days.
55
- User tokens older than a given number of days
66
- Inactive branches (Branches not analyzed for a given number of days), excepted branches marked as "keep when inactive"
7+
There is a possibility to override the "keep when inactive" regexp if the original SonarQube one was wrong and caused many branches to be kepf
78
- Inactive pull requests (PRs not analyzed for a given number of days)
89

9-
Usage: `sonar-housekeeper [-P <days>] [-B <days>] [-R <days>] [-T <days>] [--mode delete]`
10+
Usage: `sonar-housekeeper [-P <days>] [-B <days>] [-R <days>] [-T <days>] [--keepWhenInactive <regexp] [--mode delete]`
1011

1112
- `-P <days>`: Will search for projects not analyzed since more than `<days>` days.
1213
To avoid deleting too recent projects it is denied to specify less than 90 days
1314
- `-B <days>`: Will search for projects branches not analyzed since more than `<days>` days.
1415
Branches marked as "keep when inactive" are excluded from housekeeping
1516
- `-R <days>`: Will search for pull requests not analyzed since more than `<days>` days
1617
- `-T <days>`: Will search for tokens created since more than `<days>` days
18+
- `--keepWhenInactive <regexp>`: Overrides the SonarQube `sonar.dbcleaner.branchesToKeepWhenInactive` to with another regexp to consider branches to delete
1719
- `--mode delete`: If not specified, `sonar-housekeeper` will only perform a dry run and list projects
1820
branches, pull requests and tokens that would be deleted.
1921
If `--mode delete` is specified objects are actually deleted
2022
- `-h`, `-u`, `-t`, `-o`, `-v`, `-l`, `--httpTimeout`, `--threads`, `--clientCert`: See **sonar-tools** [common parameters](https://github.com/okorach/sonar-tools/blob/master/README.md)
2123

24+
## Examples
2225

23-
:warning: **sonar-tools** 2.7 or higher is required for `sonar-housekeeper` compatibility with SonarQube Server 10
26+
```
27+
export SONAR_HOST_URL=https://sonar.acme-corp.com
28+
export SONAR_TOKEN=squ_XXXXXXYYYYYZZZAAAABBBBCCCDDDEEEFFFGGGGGG
29+
30+
# Deletes all branches that have not been analyzed for 90 days and where the branch name does not match '(main|master|release|develop).*'
31+
sonar-housekeeper -B 90 --keepWhenInactive '(main|master|release|develop).*' --mode delete
32+
33+
# Lists projects that have not been analyzed on any branch or PR in the last 365 days, on SonarQube Cloud
34+
sonar-housekeeper -P 365 -u https://sonarcloud.io -t <token> -o <organization>
35+
36+
# Deletes projects that have not been analyzed on any branch or PR in the last 365 days
37+
sonar-housekeeper -P 365 --mode delete
38+
39+
# Deletes tokens created more than 365 days ago
40+
sonar-housekeeper -T 365 --mode delete
41+
```
2442

2543
## Required Permissions
2644

@@ -41,8 +59,3 @@ To avoid bad mistakes (mistakenly deleting too many projects), the tools will re
4159

4260
### :warning: Database backup
4361
**A database backup should always be taken before executing this script. There is no recovery.**
44-
45-
### Example
46-
```
47-
sonar-housekeeper -u https://sonar.acme-corp.com -t 15ee09df11fb9b8234b7a1f1ac5fce2e4e93d75d
48-
```

sonar/branches.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -361,15 +361,20 @@ def __audit_never_analyzed(self) -> list[Problem]:
361361

362362
def __audit_last_analysis(self, audit_settings: types.ConfigSettings) -> list[Problem]:
363363
if self.is_main():
364-
log.debug("%s is main (not purgeable)", str(self))
364+
log.info("%s is main (not purgeable)", str(self))
365365
return []
366366
if (age := util.age(self.last_analysis())) is None:
367367
log.debug("%s last analysis audit is disabled, skipped...", str(self))
368368
return []
369369
max_age = audit_settings.get("audit.projects.branches.maxLastAnalysisAge", 30)
370+
preserved = audit_settings.get("audit.projects.branches.keepWhenInactive", None)
370371
problems = []
371-
if self.is_kept_when_inactive():
372-
log.debug("%s is kept when inactive (not purgeable)", str(self))
372+
log.info("EVALUATING %s age %d greater than %d days and not matches '%s'", str(self), age, max_age, preserved)
373+
if preserved is not None and age > max_age and not re.match(rf"^{preserved}$", self.name):
374+
log.info("%s age %d greater than %d days and not matches '%s'", str(self), age, max_age, preserved)
375+
problems.append(Problem(get_rule(RuleId.BRANCH_LAST_ANALYSIS), self, str(self), age))
376+
elif self.is_kept_when_inactive():
377+
log.info("%s is kept when inactive (not purgeable)", str(self))
373378
elif age > max_age:
374379
problems.append(Problem(get_rule(RuleId.BRANCH_LAST_ANALYSIS), self, str(self), age))
375380
else:

test/unit/test_housekeeper.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
sonar-housekeeper tests
2525
"""
2626

27+
from collections.abc import Generator
28+
import pytest
29+
2730
import utilities as tutil
2831
from sonar import errcodes
2932
from cli import housekeeper, options
@@ -36,3 +39,14 @@ def test_housekeeper() -> None:
3639
"""test_housekeeper"""
3740
for opts in __GOOD_OPTS:
3841
assert tutil.run_cmd(housekeeper.main, f"{CMD} {tutil.SQS_OPTS} {opts}") == errcodes.OK
42+
43+
def test_keep_branches_override(csv_file: Generator[str]) -> None:
44+
"""test_keep_branches_override"""
45+
if tutil.SQ.version() == "community":
46+
pytest.skip("No branches in Community")
47+
assert tutil.run_cmd(housekeeper.main, f"{CMD} {tutil.SQS_OPTS} -P 730 -T 730 -R 730 -B 90 -f {csv_file}") == errcodes.OK
48+
nbr_br = tutil.csv_col_count_values(csv_file, 1, "BRANCH_LAST_ANALYSIS")
49+
assert tutil.run_cmd(housekeeper.main, f"{CMD} {tutil.SQS_OPTS} -P 730 -T 730 -R 730 -B 90 -f {csv_file} --keepWhenInactive 'dontkeepanything'") == errcodes.OK
50+
# With 'dontkeepanything' as branch regexp, more branches to delete should be found
51+
assert tutil.csv_col_count_values(csv_file, 1, "BRANCH_LAST_ANALYSIS") > nbr_br
52+

test/unit/utilities.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,15 @@ def csv_col_has_values(csv_file: str, col_name: str, *values) -> bool:
296296
return False
297297

298298

299-
def csv_col_count_values(csv_file: str, col_name: str, *values) -> int:
299+
def csv_col_count_values(csv_file: str, col_name_or_nbr: Union[str, int], *values) -> int:
300+
"""Counts the number of times a given column has one of the given values"""
300301
values_to_search = list(values).copy()
301302
with open(csv_file, encoding="utf-8") as fd:
302-
(col,) = get_cols(next(reader := csv.reader(fd)), col_name)
303+
reader = csv.reader(fd)
304+
if isinstance(col_name_or_nbr, int):
305+
col = col_name_or_nbr - 1
306+
else:
307+
(col,) = get_cols(next(reader, col_name_or_nbr))
303308
counter = sum(1 if line[col] in values_to_search else 0 for line in reader)
304309
return counter
305310

0 commit comments

Comments
 (0)