argocd-source-lint
Static linter to detect silent failures in multi-source ArgoCD
Application resources: resources never synced, broken $values,
missing ignoreDifferences, phantom targets.
These are the failures ArgoCD itself stays quiet about: the Application
shows healthy and synced while a manifest sits in the repo unreferenced,
a $ref points nowhere, or two Applications fight over the same file.
argocd-source-lint analyzes the Git repo it runs in (the checkout
already present in CI) — no cluster access, no kubeconfig, no
sensitive data involved.
Contents
- What the tool does
- How it fits together
- What the tool does not do (v1)
- Installation
- Usage
- Configuration
- Adopting on an existing repo
- CI/CD integration
- Development
- Changelog
- Security policy
- License
What the tool does
| Rule | Default severity | Detects |
|---|---|---|
orphan-source |
error | a manifest present in the repo but not covered by any declared source |
broken-values-ref |
error | a Helm valueFiles entry (plain, or $ref) pointing to a file that doesn't exist |
missing-ignore-diff |
warning | a known at-risk CRD (CNPG, cert-manager, Elastic ECK, RabbitMQ, Strimzi, Zalando Postgres Operator...) with no ignoreDifferences while selfHeal: true is active |
phantom-target |
error | a targetRevision/path that resolves to nothing in the repo |
unresolvable-generator |
info | an ApplicationSet generator this tool can't resolve from a local checkout alone (live cluster/API access, or goTemplate: true rendering) |
double-coverage |
error | a file covered by more than one different Application at once, each syncing it from an independent loop |
revision-mismatch |
info | a source's targetRevision differs from the checked-out revision — resolved against a snapshot of that revision, this is an FYI, not a correctness caveat |
project-scope-violation |
error | an Application's source or destination is outside the sourceRepos/destinations scope of its own AppProject |
hpa-selfheal-conflict |
warning | a HorizontalPodAutoscaler and selfHeal: true both managing spec.replicas without ignoreDifferences and the RespectIgnoreDifferences sync option — ArgoCD resets the HPA's replica count on every sync |
sync-validation-disabled |
info | Validate=false sync option — an invalid manifest is applied anyway instead of blocking |
duplicate-application-name |
error | two or more Applications share the same namespace+name — ArgoCD keys an Application by that pair, so one silently overwrites/fights the other |
malformed-ignore-diff-pointer |
warning | an ignoreDifferences jsonPointers entry doesn't start with / (RFC 6901) — it matches nothing, so the field isn't actually ignored |
malformed-ignore-diff-jq-expression |
warning | an ignoreDifferences jqPathExpressions entry doesn't start with . — fails to compile as jq, voiding the entire ignoreDifferences list for that Application, not just this entry |
unknown-sync-option |
warning | a syncOptions entry (Application-level or the per-resource sync-options annotation) doesn't match any ArgoCD-recognized key (case-sensitive) — likely a typo, silently ignored instead of erroring |
unknown-resource-hook |
warning | an argocd.argoproj.io/hook/hook-delete-policy annotation value doesn't match any ArgoCD-recognized value — likely a typo, silently falls through instead of erroring |
malformed-sync-wave |
warning | an argocd.argoproj.io/sync-wave annotation value isn't a valid integer — silently falls back to wave 0 instead of erroring |
Sample run, table output (the default):
$ argocd-source-lint .
Rule Severity File Application Message
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
phantom-target error apps/payments/application.yaml payments-service targetRevision "release-3.2"
does not resolve to any
local ref
orphan-source error manifests/legacy/old-ingress.yaml - not covered by any
declared source
double-coverage error manifests/shared/configmap.yaml - covered by both "team-a"
and "team-b"
missing-ignore-diff warning apps/postgres/application.yaml postgres-cluster selfHeal: true with no
ignoreDifferences on a
CNPG Cluster
4 findings (2 error, 1 warning, 0 info) — exit code 1
How it fits together
flowchart LR
A["Git repo checkout"] --> B["Discovery<br/>Applications + ApplicationSets"]
B --> C["Rules<br/>orphan-source, broken-values-ref,<br/>missing-ignore-diff, phantom-target,<br/>unresolvable-generator, double-coverage,<br/>revision-mismatch, project-scope-violation,<br/>hpa-selfheal-conflict, sync-validation-disabled,<br/>duplicate-application-name, malformed-ignore-diff-pointer,<br/>malformed-ignore-diff-jq-expression, unknown-sync-option,<br/>unknown-resource-hook, malformed-sync-wave"]
C --> D{"Policy<br/>severity overrides + baseline"}
D --> E["Reporters<br/>table, json, sarif, gitlab-codequality, junit"]
Everything left of the policy step is pure filesystem/git reading — no network call, no credential, so it runs in any CI job that already has a checkout. Each rule is listed in full in the table above.
What the tool does not do (v1)
- No cluster access.
- No Kustomize rendering (bases merged, patches applied) — delegate to
kustomize build/a dedicated linter for that.orphan-sourcedoes readkustomization.yaml'sresources/bases/components/patches/ generators to know which files in the overlay are actually referenced, the same signal as a plain directory source (see DESIGN.md); a remote resource reference is silently skipped, not guessed at. ApplicationSetsupport coverslist,git(directories/files),matrixandmergegenerators — each generatedApplicationgoes through the same rules as a plain one.clusters/scmProvider/pullRequest/plugingenerators,goTemplate: truerendering, and a generator's ownselector(label filter) require live cluster/API access or logic this tool doesn't reimplement — out of scope v1, flaggedunresolvable-generatorrather than guessed at.- Multi-repo
Applicationresources (sources pointing to a repo other than the one analyzed) are detected and flaggedinfo, never checked nor silently ignored.
Installation
pip install argocd-source-lint
Currently published to TestPyPI only, not yet the real PyPI index (see Development for the TestPyPI install command). The GitHub Action and GitLab CI component below both
pip installfrom the real PyPI index and will start working end-to-end once the package lands there. The pre-commit hook already works today — pre-commit builds it directly from this Git repo (pip install .), never from PyPI.
Usage
# From the root of the repo to analyze
argocd-source-lint .
# Output formats: table (default), json, sarif, gitlab-codequality, junit
argocd-source-lint . --format sarif --output results.sarif
Exit code: 0 if no blocking finding, 1 otherwise. An error finding
always blocks; an unverifiable one (e.g. a Git revision not locally
resolvable — incomplete shallow clone) blocks by default, configurable via
unverifiable_blocks_ci.
Configuration
Copy .argocd-lint.example.yaml to
.argocd-lint.yaml at the root of the repo to analyze. Any omitted key
keeps its default value:
scan_roots:
- manifests/
- infra/
rules:
orphan-source: error
broken-values-ref: error
missing-ignore-diff: warning
phantom-target: error
unresolvable-generator: info
double-coverage: error
revision-mismatch: info
project-scope-violation: error
hpa-selfheal-conflict: warning
sync-validation-disabled: info
duplicate-application-name: error
malformed-ignore-diff-pointer: warning
malformed-ignore-diff-jq-expression: warning
unknown-sync-option: warning
unknown-resource-hook: warning
malformed-sync-wave: warning
unverifiable_blocks_ci: true
exclude_paths:
- manifests/legacy/**
# Additional operator signatures, on top of the built-in pack
# (CNPG, cert-manager, Elastic ECK, RabbitMQ, Strimzi, Zalando
# Postgres Operator, Keycloak Operator, External Secrets Operator)
# — never a replacement.
known_operators:
- crd_trigger: my-operator.io/MyCRD
name_from: metadata.name
expect_ignore_on:
- kind: Secret
name_pattern: "{name}-credentials"
Adopting on an existing repo
A first run on a large, existing repo will likely surface pre-existing issues (a decommissioned component never archived, a manifest applied out-of-band). Accept them once so only new findings block CI from now on:
argocd-source-lint . --write-baseline
This writes .argocd-lint-baseline.yaml — commit it. It's plain YAML
(rule, file, application, message), meant to be reviewed like any other
file, not a hash lockfile. Re-run --write-baseline any time you want to
accept the current state again (it overwrites the file outright).
CI/CD integration
GitHub Actions
The action uploads its SARIF report via github/codeql-action/upload-sarif,
which needs its own permissions on the job — GitHub won't grant them to a
composite action automatically:
jobs:
lint:
runs-on: ubuntu-latest
permissions:
security-events: write # required for upload-sarif
actions: read # private repos only
contents: read # private repos only
steps:
- uses: actions/checkout@v4
- uses: gillesl-dev/argocd-source-lint@v1.0.0
with:
path: .
GitLab CI
A component: include only works within the same GitLab instance as
the consuming project, and this repo is hosted on GitHub — so copy
templates/lint.yml into a project in your own
GitLab group first, alongside a README.md at that project's root
(GitLab's structural requirement for a component repository). No need
to publish it to the CI/CD Catalog — that's an optional, separate step
for public discoverability, not a prerequisite for include:. Any
ref works after that (a tag, a branch, a commit SHA); replace
<your-gitlab-group> and the ref below accordingly:
include:
- component: $CI_SERVER_FQDN/<your-gitlab-group>/argocd-source-lint/lint@v1.0.0
inputs:
scope: manifests/
pre-commit
repos:
- repo: https://github.com/gillesl-dev/argocd-source-lint
rev: v1.0.0
hooks:
- id: argocd-source-lint
Development
uv sync --extra dev
uv run argocd-source-lint .
uv run pytest
To try a TestPyPI-published version locally instead of the extra dependencies above:
pip install --index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
argocd-source-lint
See DESIGN.md for the reasoning behind the mono-repo v1
scope, the directory.recurse/include/exclude semantics, and other
non-obvious decisions, CONTRIBUTING.md to add a
rule or an operator signature, and CHANGELOG.md for
the version history.
License
MIT — see LICENSE.
Metadata
Release files for argocd-source-lint 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| argocd_source_lint-1.0.0.tar.gz | 131.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| argocd_source_lint-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 202.2 kB
Release files / argocd_source_lint-1.0.0.tar.gz
| Download URL | argocd_source_lint-1.0.0.tar.gz |
|---|---|
| Size | 131.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6804f220b84e67518cb6140401575c2dd2ce585a3bdb6de91e502cda5884658c
|
|
BLAKE2b-256 checksum How to use checksums |
5088bdef25389e142bd1db6af47e1d54d9262dc4fe63c28d1040068af867ab99
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|
Release files / argocd_source_lint-1.0.0-py3-none-any.whl
| Download URL | argocd_source_lint-1.0.0-py3-none-any.whl |
|---|---|
| Size | 70.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ad92505ffafb7bfe02e2b11533a19e1b99cef3e1c930b0da64c7479becb2b6de
|
|
BLAKE2b-256 checksum How to use checksums |
1251083e0b0b4419e62cfc8e0e316d0f7312c58018c9bf2f7203a47da8acf7e1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.3
|