argocd-source-lint 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. argocd_source_lint-1.0.0/.argocd-lint.example.yaml +31 -0
  2. argocd_source_lint-1.0.0/.github/workflows/test.yml +49 -0
  3. argocd_source_lint-1.0.0/.gitignore +11 -0
  4. argocd_source_lint-1.0.0/.pre-commit-hooks.yaml +7 -0
  5. argocd_source_lint-1.0.0/CHANGELOG.md +508 -0
  6. argocd_source_lint-1.0.0/CONTRIBUTING.md +101 -0
  7. argocd_source_lint-1.0.0/DESIGN.md +1133 -0
  8. argocd_source_lint-1.0.0/LICENSE +21 -0
  9. argocd_source_lint-1.0.0/PKG-INFO +306 -0
  10. argocd_source_lint-1.0.0/README.md +279 -0
  11. argocd_source_lint-1.0.0/SECURITY.md +58 -0
  12. argocd_source_lint-1.0.0/action.yml +61 -0
  13. argocd_source_lint-1.0.0/pyproject.toml +64 -0
  14. argocd_source_lint-1.0.0/src/argocd_source_lint/__init__.py +10 -0
  15. argocd_source_lint-1.0.0/src/argocd_source_lint/app_projects.py +53 -0
  16. argocd_source_lint-1.0.0/src/argocd_source_lint/applicationset.py +451 -0
  17. argocd_source_lint-1.0.0/src/argocd_source_lint/baseline.py +94 -0
  18. argocd_source_lint-1.0.0/src/argocd_source_lint/cli.py +221 -0
  19. argocd_source_lint-1.0.0/src/argocd_source_lint/coverage.py +284 -0
  20. argocd_source_lint-1.0.0/src/argocd_source_lint/fsutil.py +199 -0
  21. argocd_source_lint-1.0.0/src/argocd_source_lint/git_context.py +301 -0
  22. argocd_source_lint-1.0.0/src/argocd_source_lint/globs.py +70 -0
  23. argocd_source_lint-1.0.0/src/argocd_source_lint/loader.py +153 -0
  24. argocd_source_lint-1.0.0/src/argocd_source_lint/models.py +125 -0
  25. argocd_source_lint-1.0.0/src/argocd_source_lint/policy.py +60 -0
  26. argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/__init__.py +0 -0
  27. argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/fingerprint.py +19 -0
  28. argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/gitlab_codequality.py +36 -0
  29. argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/json_report.py +18 -0
  30. argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/junit.py +67 -0
  31. argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/sarif.py +125 -0
  32. argocd_source_lint-1.0.0/src/argocd_source_lint/reporters/table.py +53 -0
  33. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/__init__.py +0 -0
  34. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/base.py +37 -0
  35. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/broken_values_ref.py +199 -0
  36. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/double_coverage.py +64 -0
  37. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/duplicate_application_name.py +67 -0
  38. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/hpa_selfheal_conflict.py +153 -0
  39. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/known-operators.yaml +89 -0
  40. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/malformed_ignore_diff_jq_expression.py +62 -0
  41. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/malformed_ignore_diff_pointer.py +55 -0
  42. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/malformed_sync_wave.py +79 -0
  43. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/missing_ignore_diff.py +181 -0
  44. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/orphan_source.py +105 -0
  45. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/phantom_target.py +73 -0
  46. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/project_scope.py +204 -0
  47. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/revision_mismatch.py +61 -0
  48. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/sync_validation_disabled.py +53 -0
  49. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/unknown_resource_hook.py +116 -0
  50. argocd_source_lint-1.0.0/src/argocd_source_lint/rules/unknown_sync_option.py +151 -0
  51. argocd_source_lint-1.0.0/templates/lint.yml +38 -0
  52. argocd_source_lint-1.0.0/tests/conftest.py +92 -0
  53. argocd_source_lint-1.0.0/tests/fixtures/bad_broken_values_ref/bootstrap/argocd-apps/broken-ref-file-app.yaml +15 -0
  54. argocd_source_lint-1.0.0/tests/fixtures/bad_broken_values_ref/bootstrap/argocd-apps/broken-ref-name-app.yaml +12 -0
  55. argocd_source_lint-1.0.0/tests/fixtures/bad_missing_ignore_diff/bootstrap/argocd-apps/pg-app.yaml +13 -0
  56. argocd_source_lint-1.0.0/tests/fixtures/bad_missing_ignore_diff/infrastructure/pg/cluster.yaml +7 -0
  57. argocd_source_lint-1.0.0/tests/fixtures/bad_orphan_source/.argocd-lint.yaml +2 -0
  58. argocd_source_lint-1.0.0/tests/fixtures/bad_orphan_source/bootstrap/argocd-apps/demo-app.yaml +13 -0
  59. argocd_source_lint-1.0.0/tests/fixtures/bad_orphan_source/manifests/demo-app/deployment.yaml +6 -0
  60. argocd_source_lint-1.0.0/tests/fixtures/bad_orphan_source/manifests/ignored-orphan/configmap.yaml +7 -0
  61. argocd_source_lint-1.0.0/tests/fixtures/bad_orphan_source/manifests/orphaned/configmap.yaml +6 -0
  62. argocd_source_lint-1.0.0/tests/fixtures/bad_phantom_target/bootstrap/argocd-apps/demo-app.yaml +10 -0
  63. argocd_source_lint-1.0.0/tests/fixtures/good_repo/.argocd-lint.yaml +2 -0
  64. argocd_source_lint-1.0.0/tests/fixtures/good_repo/bootstrap/argocd-apps/demo-app.yaml +13 -0
  65. argocd_source_lint-1.0.0/tests/fixtures/good_repo/manifests/demo-app/deployment.yaml +6 -0
  66. argocd_source_lint-1.0.0/tests/fixtures/good_repo/manifests/demo-app/service.yaml +7 -0
  67. argocd_source_lint-1.0.0/tests/test_applicationset.py +976 -0
  68. argocd_source_lint-1.0.0/tests/test_baseline.py +89 -0
  69. argocd_source_lint-1.0.0/tests/test_cli.py +261 -0
  70. argocd_source_lint-1.0.0/tests/test_coverage.py +243 -0
  71. argocd_source_lint-1.0.0/tests/test_fsutil.py +144 -0
  72. argocd_source_lint-1.0.0/tests/test_git_context.py +191 -0
  73. argocd_source_lint-1.0.0/tests/test_globs.py +72 -0
  74. argocd_source_lint-1.0.0/tests/test_loader.py +340 -0
  75. argocd_source_lint-1.0.0/tests/test_policy.py +51 -0
  76. argocd_source_lint-1.0.0/tests/test_reporters.py +265 -0
  77. argocd_source_lint-1.0.0/tests/test_rules_broken_values_ref.py +309 -0
  78. argocd_source_lint-1.0.0/tests/test_rules_double_coverage.py +213 -0
  79. argocd_source_lint-1.0.0/tests/test_rules_duplicate_application_name.py +207 -0
  80. argocd_source_lint-1.0.0/tests/test_rules_hpa_selfheal_conflict.py +246 -0
  81. argocd_source_lint-1.0.0/tests/test_rules_malformed_ignore_diff_jq_expression.py +141 -0
  82. argocd_source_lint-1.0.0/tests/test_rules_malformed_ignore_diff_pointer.py +137 -0
  83. argocd_source_lint-1.0.0/tests/test_rules_malformed_sync_wave.py +134 -0
  84. argocd_source_lint-1.0.0/tests/test_rules_missing_ignore_diff.py +648 -0
  85. argocd_source_lint-1.0.0/tests/test_rules_orphan_source.py +416 -0
  86. argocd_source_lint-1.0.0/tests/test_rules_phantom_target.py +179 -0
  87. argocd_source_lint-1.0.0/tests/test_rules_project_scope.py +423 -0
  88. argocd_source_lint-1.0.0/tests/test_rules_revision_mismatch.py +184 -0
  89. argocd_source_lint-1.0.0/tests/test_rules_sync_validation_disabled.py +94 -0
  90. argocd_source_lint-1.0.0/tests/test_rules_unknown_resource_hook.py +136 -0
  91. argocd_source_lint-1.0.0/tests/test_rules_unknown_sync_option.py +217 -0
  92. argocd_source_lint-1.0.0/tests/test_version.py +18 -0
  93. argocd_source_lint-1.0.0/uv.lock +340 -0
@@ -0,0 +1,31 @@
1
+ # Copy to .argocd-lint.yaml at the repo root and adapt.
2
+ # Any omitted key keeps its default value.
3
+
4
+ scan_roots:
5
+ - manifests/
6
+ - infra/
7
+
8
+ rules:
9
+ orphan-source: error
10
+ broken-values-ref: error
11
+ missing-ignore-diff: warning
12
+ phantom-target: error
13
+ unresolvable-generator: info
14
+ double-coverage: error
15
+ revision-mismatch: info
16
+ project-scope-violation: error
17
+ hpa-selfheal-conflict: warning
18
+ sync-validation-disabled: info
19
+ duplicate-application-name: error
20
+ malformed-ignore-diff-pointer: warning
21
+ malformed-ignore-diff-jq-expression: warning
22
+ unknown-sync-option: warning
23
+ unknown-resource-hook: warning
24
+ malformed-sync-wave: warning
25
+
26
+ # Default: true (fail-closed). Only set to false knowingly if the team
27
+ # accepts running on an incomplete shallow clone in CI.
28
+ unverifiable_blocks_ci: true
29
+
30
+ exclude_paths:
31
+ - manifests/legacy/**
@@ -0,0 +1,49 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ lint:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Install uv
15
+ uses: astral-sh/setup-uv@v4
16
+ with:
17
+ python-version: "3.11"
18
+
19
+ - name: Install dependencies
20
+ run: uv sync --extra dev
21
+
22
+ - name: Ruff check
23
+ run: uv run ruff check .
24
+
25
+ - name: Ruff format check
26
+ run: uv run ruff format --check .
27
+
28
+ pytest:
29
+ strategy:
30
+ fail-fast: false
31
+ matrix:
32
+ os: [ubuntu-latest, windows-latest]
33
+ python-version: ["3.11", "3.12", "3.13"]
34
+
35
+ runs-on: ${{ matrix.os }}
36
+
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+
40
+ - name: Install uv
41
+ uses: astral-sh/setup-uv@v4
42
+ with:
43
+ python-version: ${{ matrix.python-version }}
44
+
45
+ - name: Install dependencies
46
+ run: uv sync --extra dev
47
+
48
+ - name: Run tests
49
+ run: uv run pytest -q
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+
10
+ # Internal planning notes — never published (see §13 of the plan).
11
+ argocd-source-lint-plan-technique.md
@@ -0,0 +1,7 @@
1
+ - id: argocd-source-lint
2
+ name: argocd-source-lint
3
+ description: Detects silent failures in multi-source ArgoCD Applications.
4
+ entry: argocd-source-lint
5
+ language: python
6
+ pass_filenames: false
7
+ always_run: true
@@ -0,0 +1,508 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file, in
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format.
5
+
6
+ ## [1.0.0] - 2026-09-22
7
+
8
+ ### Changed
9
+
10
+ - Version bumped to `1.0.0`, marking the public v1 launch (real PyPI +
11
+ GitHub Marketplace). No functional change from `0.1.36` — every rule,
12
+ reporter, and the two security-audit rounds behind it are unchanged;
13
+ this is the version/metadata declaration that milestone was gated on.
14
+ PyPI classifier updated from `Development Status :: 3 - Alpha` to
15
+ `5 - Production/Stable`.
16
+
17
+ ## [0.1.36] - 2026-09-22
18
+
19
+ ### Security
20
+
21
+ A second, more thorough audit pass (explicitly requested: "tu en vois
22
+ d'autres ? ... je veux en être sûr"), checked systematically by
23
+ category rather than opportunistically. Three more real, confirmed
24
+ issues:
25
+
26
+ - `materialize_revision` crashed the whole CLI on a hostile tar
27
+ stream instead of returning `None`. `filter="data"` (PEP 706)
28
+ correctly rejects a `../`-style tar entry — confirmed for real, it
29
+ raises `OutsideDestinationError` rather than extracting outside the
30
+ target directory — but that rejection is itself an exception, and
31
+ nothing caught it. Now wrapped in `try: ... except tarfile.TarError`,
32
+ same effect as the existing "subprocess failed" branch beside it.
33
+ - The table reporter interpreted repo-controlled content (an
34
+ Application's own name, a filename, a rule's message) as `rich`
35
+ markup. Confirmed for real: a crafted `metadata.name` containing
36
+ `[link=...]` rendered as an actual clickable hyperlink, and a
37
+ `[bold red on white]`-style tag actually re-styled the row — a
38
+ crafted repo could restyle or spoof this tool's own terminal output.
39
+ Every field except the severity cell (built from this tool's own
40
+ closed enum, never repo content) is now escaped with
41
+ `rich.markup.escape()`.
42
+ - The ApplicationSet `matrix` generator's cartesian product had no
43
+ size cap — only the "max 2 children" structural cap existed, nothing
44
+ about how large those two children's own param lists could be.
45
+ Confirmed for real: two `list` generators of 5,000 small elements
46
+ each (individually well under the alias-bomb node budget, since that
47
+ budget catches a densely aliased document, not a large flat one)
48
+ produced 25,000,000 combinations in ~7s for the combine step alone,
49
+ before a single generated Application is even built. Capped at
50
+ 10,000 combinations, checked before the cartesian product is built.
51
+
52
+ Checked and confirmed not vulnerable: `ruamel.yaml`'s loaders don't
53
+ execute Python object construction from YAML tags; the JUnit/SARIF/
54
+ JSON/GitLab reporters all escape properly (`ElementTree`/`json.dumps`,
55
+ never manual string concatenation); `pip-audit` found nothing against
56
+ the exact resolved dependency versions.
57
+
58
+ Documented as a limitation, not a code fix: `.argocd-lint.yaml` and
59
+ `.argocd-lint-baseline.yaml` are repo content themselves, so an
60
+ untrusted fork's PR can edit either in the same PR that introduces
61
+ what it would otherwise flag — protect both with `CODEOWNERS`/branch
62
+ protection if running against untrusted forks.
63
+
64
+ ## [0.1.35] - 2026-09-22
65
+
66
+ ### Security
67
+
68
+ - `globs.py`'s `match_glob` (shared by the ApplicationSet `git`
69
+ generator's `directories`/`files` `path:` and
70
+ `project-scope-violation`'s `sourceRepos`/`destinations`) translated
71
+ a glob pattern into a backtracking regex. Confirmed for real: a
72
+ pattern shaped like `*a*a*a...*a!` (~40 repetitions) against a
73
+ non-matching candidate of `a`s hung indefinitely — a classic ReDoS,
74
+ and both the pattern and the candidate are repo-controlled here.
75
+ `match_glob` now tokenizes the pattern once and runs a
76
+ dynamic-programming scan over the candidate instead —
77
+ `O(len(pattern) x len(candidate))` by construction, regardless of how
78
+ many wildcards the pattern has, rather than relying on an input-size
79
+ limit that would still be exponential below the limit.
80
+
81
+ ## [0.1.34] - 2026-09-22
82
+
83
+ ### Security
84
+
85
+ - A source's own `path` (e.g. `path: ../outside-secret`) was joined
86
+ onto the repo root with no check that the result stayed inside it.
87
+ Confirmed for real: this crashed `covered_files_for_application`
88
+ (`orphan-source`, `double-coverage`) and made
89
+ `covered_documents_for_application` (`missing-ignore-diff`,
90
+ `hpa-selfheal-conflict`, `unknown-sync-option`,
91
+ `unknown-resource-hook`, `malformed-sync-wave`) silently read and
92
+ surface a file's full content from anywhere reachable via `../` from
93
+ the repo root — a straight local file disclosure, not just a crash.
94
+ - A second, independent instance of the same bug: a Kustomize overlay's
95
+ own `resources`/`bases`/`components`/`crds`/`patches*`/generator
96
+ entries were resolved the same unguarded way. This one comes from
97
+ *tracked YAML content*, not ArgoCD's own schema, so it doesn't even
98
+ need a crafted `Application` — any commit touching a
99
+ `kustomization.yaml` can reach it, and the recursion means a nested
100
+ overlay chain could walk arbitrarily far outside the repo.
101
+ - Both fixed by threading the repo/snapshot root through the whole
102
+ resolution chain and checking every resolved path against it
103
+ (`Path.is_relative_to`) before it's ever stat'd, walked, or read —
104
+ never filtered out of the result afterward, which would still touch
105
+ arbitrary filesystem locations along the way.
106
+
107
+ ## [0.1.33] - 2026-09-22
108
+
109
+ ### Security
110
+
111
+ - A `targetRevision` (or an ApplicationSet `git` generator's own
112
+ `revision`) is repo-controlled YAML, but was passed straight through
113
+ as a positional argument to `git`. A value like
114
+ `--remote=https://<host>/x` is read by `git` as a *flag*, not a
115
+ revision — confirmed for real: `git archive` with that value spends
116
+ the full TCP connect timeout actually reaching out to `<host>`
117
+ instead of failing to parse, a live SSRF primitive from inside
118
+ whatever CI job runs this tool, breaking the "no network access, no
119
+ credentials" guarantee the tool is built on. Every function in
120
+ `git_context.py` that shells out with a `revision`
121
+ (`is_revision_resolvable`, `_resolve_commit`, `materialize_revision`,
122
+ `list_tree_paths`) now rejects a `-`-prefixed value before it ever
123
+ reaches a `git` subprocess — a real revision never starts with `-`
124
+ in the first place, so no legitimate value is affected. A rejected
125
+ revision is treated exactly like any other unresolvable one, same
126
+ `unverifiable` handling already in place.
127
+
128
+ ## [0.1.32] - 2026-09-22
129
+
130
+ ### Added
131
+
132
+ - `malformed-ignore-diff-jq-expression` rule: an `ignoreDifferences`
133
+ `jqPathExpressions` entry that doesn't start with `.` — every real
134
+ example in ArgoCD's own docs does, the same convention `jsonPointers`
135
+ has for `/`. Usually a `jsonPointers`-style path pasted into the
136
+ wrong sibling field. Confirmed against ArgoCD's own source
137
+ (`NewIgnoreNormalizer`): a parse failure here voids the *entire*
138
+ `ignoreDifferences` list for that Application, not just this entry —
139
+ a wider blast radius than a bad `jsonPointers` entry, and matching
140
+ real reports of `jqPathExpressions` that "apply without errors but
141
+ don't actually do anything." Deliberately as narrow as
142
+ `malformed-ignore-diff-pointer`: no `jq` engine involved, no attempt
143
+ to catch ArgoCD's own version-dependent evaluation quirks.
144
+
145
+ ### Fixed
146
+
147
+ - `policy.DEFAULT_RULE_SEVERITIES` and `reporters/sarif.py`'s
148
+ `_RULES_METADATA` are two more manually maintained rule-ID lists,
149
+ same shape as the `cli.RULES` gap closed in `0.1.31` — a rule
150
+ missing from either wouldn't crash, just silently lose its
151
+ configurable severity or its SARIF `fullDescription`/`helpUri`. Two
152
+ new tests assert both stay in sync with the real rule set.
153
+
154
+ ## [0.1.31] - 2026-09-22
155
+
156
+ ### Fixed
157
+
158
+ - Five rules (`missing-ignore-diff`, `hpa-selfheal-conflict`,
159
+ `unknown-sync-option`, `unknown-resource-hook`, `malformed-sync-wave`)
160
+ and two more (`orphan-source`, `double-coverage`) each independently
161
+ re-walked and re-parsed the same Application's covered source
162
+ directory — confirmed for real: 40 Applications x 15 covered files
163
+ each, across just 3 of those rules, cost ~4.9s of pure re-walking and
164
+ re-parsing. `fsutil.load_yaml_documents` now caches by resolved file
165
+ path and `coverage.covered_files_for_source` caches by
166
+ `(source_dir, directory_recurse, directory_include,
167
+ directory_exclude)` — both reset via `clear_caches`, same story as
168
+ every other cache here.
169
+ - `phantom-target` ran one `git ls-tree` subprocess per Application to
170
+ check its `path` exists at `targetRevision`, even though most
171
+ Applications in a real repo share the same revision — confirmed for
172
+ real: 40 Applications with distinct paths cost ~2.3s of subprocess
173
+ spawns. `git_context.tree_paths_at_revision` now lists the whole tree
174
+ once per `(repo_root, revision)`, and `path_has_tracked_files`
175
+ answers the per-path question against that in-memory listing instead.
176
+ `broken-values-ref`'s own equivalent local cache is now backed by the
177
+ same shared one.
178
+ - A `Rule` subclass that exists under `rules/` but is never added to
179
+ `cli.RULES` stayed invisible to the whole test suite: its own unit
180
+ test instantiates and calls it directly, so it could pass while the
181
+ real CLI never ran it at all. Confirmed for real with a throwaway
182
+ rule module. `test_every_rule_class_is_registered_in_cli_rules`
183
+ force-imports every module under `rules/` and asserts none are
184
+ missing from `RULES`.
185
+
186
+ ### Changed
187
+
188
+ - `orphan-source` imported `broken_values_ref.py`'s `_parse_ref_entry`
189
+ across module boundaries despite its underscore signaling "private" —
190
+ it already was shared, just not admitting it. Renamed to
191
+ `parse_ref_entry`, and the `{source.ref: source for source in
192
+ app.sources if source.ref}` comprehension duplicated verbatim in both
193
+ files is now the shared `ref_sources_by_name`. No behavior change.
194
+
195
+ ## [0.1.30] - 2026-09-21
196
+
197
+ ### Changed
198
+
199
+ - `applicationset.py`'s four generator resolvers
200
+ (`_resolve_generator`/`_resolve_git`/`_resolve_matrix`/
201
+ `_resolve_merge`) each repeated the same five parameters
202
+ (`repo_root`, `local_origin`, `appset_name`, `source_file`,
203
+ `severity`) alongside their own generator dict. Replaced with a
204
+ single `GeneratorContext`. Internal only, no behavior change, no
205
+ call site outside this file.
206
+
207
+ ## [0.1.29] - 2026-09-21
208
+
209
+ ### Fixed
210
+
211
+ - `RawManifestDiscovery`, the ApplicationSet walker and
212
+ `discover_app_projects` each independently walked the entire repo
213
+ and parsed every YAML file, each looking for a different `kind` —
214
+ three full passes over the same files. Confirmed for real: 2,000
215
+ plain manifests (none of them an Application/ApplicationSet/
216
+ AppProject) cost ~5s total across the three passes.
217
+ `fsutil.discover_documents` now does that walk once and caches it
218
+ (same scoping as `_resolve_commit`'s cache, reset via
219
+ `clear_caches`); the three callers just filter the shared result by
220
+ `kind`. Every existing call site's own signature is unchanged.
221
+
222
+ ## [0.1.28] - 2026-09-21
223
+
224
+ ### Fixed
225
+
226
+ - 50 Applications sharing `targetRevision: HEAD` cost 702 identical
227
+ `git rev-parse` calls in a single run — confirmed for real, not
228
+ estimated: ~58 seconds on Windows, where subprocess spawn dominates
229
+ the cost. Every local-source rule independently re-resolved the same
230
+ revision against the same checkout with no caching at all.
231
+ `git_context._resolve_commit` now caches by `(repo_root, revision)`
232
+ for the life of one CLI invocation, collapsing the same run to 1
233
+ call and ~5 seconds. A caching change must never alter results, only
234
+ speed — verified: identical findings before and after, and a
235
+ dedicated test confirms a repo mutated between two invocations still
236
+ gets a fresh answer (the one scenario a naive process-lifetime cache
237
+ could get wrong).
238
+
239
+ ## [0.1.27] - 2026-09-21
240
+
241
+ ### Fixed
242
+
243
+ - A directory symlink/junction pointing back at one of its own
244
+ ancestors made file discovery run forever — confirmed for real with
245
+ a Windows junction (a few KB on disk). Neither `Path.rglob` nor even
246
+ `os.walk(directory, followlinks=False)` protect against it: a
247
+ junction isn't reported as a symlink, so that guard never triggers.
248
+ `fsutil.walk_tree` replaces three independent, unguarded `rglob`
249
+ call sites (`iter_yaml_files`, and the ApplicationSet `git`
250
+ generator's own directory/file discovery) with one shared,
251
+ cycle-safe walk — each directory resolved once and never
252
+ re-descended into, the same identity-tracking already used for a
253
+ Kustomize `resources:` cycle. As a side effect, a file reachable
254
+ through two different non-cyclic paths to the same physical
255
+ directory is now found once per traversal instead of double-counted.
256
+
257
+ ## [0.1.26] - 2026-09-21
258
+
259
+ ### Fixed
260
+
261
+ - A YAML "billion laughs" alias bomb (a few hundred bytes, nested
262
+ anchors each referencing the previous one twice) hung the tool
263
+ indefinitely — confirmed for real against both an ApplicationSet
264
+ `git` generator's `files:` params file and a manifest with a bombed
265
+ `apiVersion` field. `ruamel.yaml`'s own loader is safe (an alias
266
+ resolves to the same object, not a copy); the hang came from code
267
+ downstream blindly stringifying the result. `fsutil.load_yaml_documents`
268
+ now rejects a document whose fully-expanded size would be
269
+ unreasonable (`is_within_budget`, computed via memoized recursion so
270
+ the check itself stays cheap), the same way it already drops a file
271
+ that fails to parse — closing this at the one place virtually every
272
+ document is read through, rather than patching each of the five
273
+ near-identical unguarded `str()` call sites this audit found.
274
+
275
+ ## [0.1.25] - 2026-09-21
276
+
277
+ ### Fixed
278
+
279
+ - A missing `git` binary silently degraded every result into false
280
+ `orphan-source`/`double-coverage` positives instead of a clear error:
281
+ `get_origin_url` was the only call site that handled it, by returning
282
+ `None` — the exact same value it returns for "no `origin` remote
283
+ configured," which every rule already (correctly, for that case)
284
+ treats as "nothing here is local." Reproduced against
285
+ `python:3.11-slim`, the base image `templates/lint.yml` itself
286
+ recommends, which doesn't ship `git`. Fixed on both ends: the CLI now
287
+ checks `git` is on PATH at startup and exits with a clear error
288
+ instead of degrading, and `templates/lint.yml` installs `git` in
289
+ `before_script`.
290
+
291
+ ## [0.1.24] - 2026-09-21
292
+
293
+ ### Fixed
294
+
295
+ - The ApplicationSet `git` generator's own `revision` field
296
+ (`directories`/`files`, independent of any generated Application's
297
+ `targetRevision`) was never read at all — directory/file discovery
298
+ always used the checked-out working tree. A `revision` pinned away
299
+ from HEAD would silently generate Applications from today's directory
300
+ structure instead of the pinned one's, with no finding raised at all
301
+ — worse than the other `unresolvable-generator` cases, which are at
302
+ least flagged. Now resolved against a snapshot of that revision, the
303
+ same mechanism already used for a source's own `targetRevision`
304
+ (`git_context.materialize_revision`, moved there from `coverage.py`
305
+ so both call sites share it). An unresolvable revision is reported
306
+ `unverifiable`, same as `phantom-target`/`broken-values-ref`'s
307
+ shallow-clone case.
308
+
309
+ ## [0.1.23] - 2026-09-21
310
+
311
+ ### Fixed
312
+
313
+ - SARIF's `$schema` pointed at the spec repo's mutable `master` branch
314
+ instead of GitHub's own recommended
315
+ `https://json.schemastore.org/sarif-2.1.0.json`. Results had no
316
+ `partialFingerprints`, which GitHub's docs call essential for
317
+ matching the same finding across runs — without it, an unrelated
318
+ change elsewhere can make every finding look like a new alert.
319
+ - JUnit `<testcase name=...>` was the finding's raw message, and two
320
+ different findings (different file/Application) can share that
321
+ message verbatim — GitLab's JUnit parser silently drops every
322
+ testcase after the first with a duplicate name. A `(#N)` suffix on an
323
+ exact repeat keeps every finding visible.
324
+ - README's "pending real PyPI" installation caveat wrongly included the
325
+ pre-commit hook (it builds from this Git repo via `pip install .`,
326
+ never from PyPI, so it already works today) and omitted the GitLab CI
327
+ component (which does `pip install` from the real PyPI index, and is
328
+ genuinely affected).
329
+ - README's GitHub Actions example was missing `actions/checkout` —
330
+ without it there's nothing for the action to analyze, and it silently
331
+ "succeeds" with zero findings instead of erroring — and the
332
+ `permissions: security-events: write` (`actions`/`contents: read` on
333
+ private repos) the composite action's `upload-sarif` step needs, which
334
+ GitHub never grants automatically to a composite action.
335
+ - README's GitLab CI section overstated the prerequisite: using
336
+ `include: component:` never requires publishing to the CI/CD
337
+ Catalog, only a `README.md` + `templates/` in a project on the same
338
+ GitLab instance, referenced by any ref (tag, branch or commit SHA) —
339
+ confirmed against GitLab's own component docs. Publishing to the
340
+ Catalog is a separate, optional step for public discoverability.
341
+
342
+ ### Added
343
+
344
+ - Rules now carry `fullDescription`, `helpUri` and
345
+ `defaultConfiguration.level` in the SARIF output (recommended fields
346
+ GitHub's UI uses for filtering/detail pages).
347
+
348
+ ## [0.1.22] - 2026-09-21
349
+
350
+ ### Fixed
351
+
352
+ - `directory.include`/`exclude` and `.argocd-lint.yaml`'s `exclude_paths`
353
+ now match case-sensitively. `fnmatch.fnmatch` case-folds on Windows,
354
+ which could silently diverge from ArgoCD's own case-sensitive Go
355
+ `filepath.Match` and from a case-sensitive CI runner.
356
+
357
+ ## [0.1.21] - 2026-09-21
358
+
359
+ ### Fixed
360
+
361
+ - `broken-values-ref` now also checks plain (non-`$ref`) Helm
362
+ `valueFiles` entries against the repo, not just cross-source `$ref`
363
+ entries.
364
+
365
+ ## [0.1.20] - 2026-09-21
366
+
367
+ ### Added
368
+
369
+ - `malformed-sync-wave` rule: a non-integer
370
+ `argocd.argoproj.io/sync-wave` annotation value, which ArgoCD silently
371
+ falls back to wave 0 for instead of erroring.
372
+
373
+ ## [0.1.19] - 2026-09-21
374
+
375
+ ### Added
376
+
377
+ - Keycloak Operator and External Secrets Operator signatures to the
378
+ built-in `known_operators` pack.
379
+
380
+ ### Changed
381
+
382
+ - Clarified the GitLab CI `<namespace>` placeholder (renamed to
383
+ `<your-gitlab-group>`, documented the GitLab CI/CD Catalog publishing
384
+ prerequisite).
385
+
386
+ ## [0.1.18] - 2026-09-21
387
+
388
+ ### Added
389
+
390
+ - `unknown-resource-hook` rule: an unrecognized
391
+ `argocd.argoproj.io/hook`/`hook-delete-policy` annotation value.
392
+ - Zalando Postgres Operator signature to `known_operators`.
393
+
394
+ ### Changed
395
+
396
+ - `unknown-sync-option` now also checks the per-resource
397
+ `argocd.argoproj.io/sync-options` annotation, not just the
398
+ Application-level `syncOptions`.
399
+
400
+ ## [0.1.17] - 2026-09-21
401
+
402
+ ### Added
403
+
404
+ - `unknown-sync-option` rule: an Application-level `syncOptions` entry
405
+ that doesn't match any ArgoCD-recognized key.
406
+
407
+ ## [0.1.16] - 2026-09-18
408
+
409
+ ### Added
410
+
411
+ - `duplicate-application-name` rule: two or more Applications sharing
412
+ the same namespace+name.
413
+ - `malformed-ignore-diff-pointer` rule: an `ignoreDifferences`
414
+ `jsonPointers` entry that doesn't start with `/` (RFC 6901).
415
+
416
+ ## [0.1.15] - 2026-09-18
417
+
418
+ ### Added
419
+
420
+ - `sync-validation-disabled` rule: `Validate=false` in `syncOptions`.
421
+
422
+ ### Fixed
423
+
424
+ - The ApplicationSet `matrix` generator is now capped at 2 child
425
+ generators, matching ArgoCD's own limit — previously computed a bogus
426
+ N-way cartesian product beyond that.
427
+
428
+ ## [0.1.14] - 2026-09-18
429
+
430
+ ### Added
431
+
432
+ - `hpa-selfheal-conflict` rule: a `HorizontalPodAutoscaler` and
433
+ `selfHeal: true` both managing `spec.replicas` without
434
+ `ignoreDifferences` and the `RespectIgnoreDifferences` sync option.
435
+
436
+ ## [0.1.13] - 2026-09-18
437
+
438
+ ### Added
439
+
440
+ - Stale baseline entry detection: a `--write-baseline` entry no longer
441
+ matching any current finding is reported as a count, without
442
+ affecting the exit code.
443
+ - JUnit XML reporter (`--format junit`).
444
+
445
+ ## [0.1.12] - 2026-09-18
446
+
447
+ ### Added
448
+
449
+ - Elastic ECK, RabbitMQ Cluster Operator and Strimzi signatures to the
450
+ built-in `known_operators` pack.
451
+
452
+ ## [0.1.11] - 2026-09-18
453
+
454
+ ### Added
455
+
456
+ - `project-scope-violation` rule: an Application's source/destination
457
+ outside its AppProject's `sourceRepos`/`destinations` scope.
458
+
459
+ ## [0.1.10] - 2026-09-18
460
+
461
+ ### Added
462
+
463
+ - `revision-mismatch` rule (info): a source's `targetRevision` differs
464
+ from the checked-out revision.
465
+
466
+ ### Fixed
467
+
468
+ - `orphan-source`, `missing-ignore-diff` and `double-coverage` now
469
+ resolve a source against its own `targetRevision` when it drifts from
470
+ HEAD, instead of always reading the checked-out working tree — a
471
+ manifest only live at a pinned revision was previously never actually
472
+ read.
473
+
474
+ ## [0.1.9] - 2026-09-18
475
+
476
+ ### Added
477
+
478
+ - Support for the ApplicationSet `merge` generator (previously always
479
+ flagged `unresolvable-generator`).
480
+
481
+ ## [0.1.8] - 2026-09-15
482
+
483
+ ### Fixed
484
+
485
+ - An ApplicationSet generator's own `selector` (label filter) is now
486
+ flagged `unresolvable-generator` instead of silently ignored.
487
+
488
+ ## [0.1.7] - 2026-09-15
489
+
490
+ ### Added
491
+
492
+ - `double-coverage` rule: a file covered by more than one different
493
+ Application's sources at once.
494
+
495
+ ## [0.1.6] - 2026-09-15
496
+
497
+ Initial public release, published under the `gillesl-dev` GitHub/PyPI
498
+ namespace.
499
+
500
+ ### Added
501
+
502
+ - Core rules: `orphan-source`, `broken-values-ref`,
503
+ `missing-ignore-diff`, `phantom-target`, `unresolvable-generator`.
504
+
505
+ ### Fixed
506
+
507
+ - The table reporter now shows a placeholder instead of a blank cell
508
+ for `orphan-source` findings, which have no owning Application.