Skip to main content

argocd-source-lint

Tests License: MIT Python 3.11+ Ruff Status: Stable

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

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-source does read kustomization.yaml's resources/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.
  • ApplicationSet support covers list, git (directories/files), matrix and merge generators — each generated Application goes through the same rules as a plain one. clusters/scmProvider/ pullRequest/plugin generators, goTemplate: true rendering, and a generator's own selector (label filter) require live cluster/API access or logic this tool doesn't reimplement — out of scope v1, flagged unresolvable-generator rather than guessed at.
  • Multi-repo Application resources (sources pointing to a repo other than the one analyzed) are detected and flagged info, 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 install from 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)

Source distribution for argocd-source-lint 1.0.0
File Size Uploaded
argocd_source_lint-1.0.0.tar.gz 131.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for argocd-source-lint 1.0.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.2

2 release files

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page