Full Code of dadav/helm-schema for AI

main 72b95654c2af cached
77 files
300.2 KB
86.3k tokens
160 symbols
1 requests
Download .txt
Showing preview only (321K chars total). Download the full file or copy to clipboard to get everything.
Repository: dadav/helm-schema
Branch: main
Commit: 72b95654c2af
Files: 77
Total size: 300.2 KB

Directory structure:
gitextract_wnyi6ji5/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── build_and_test.yml
│       └── release.yml
├── .gitignore
├── .goreleaser.yaml
├── .pre-commit-config.yaml
├── .pre-commit-hooks.yaml
├── CLAUDE.md
├── Dockerfile
├── LICENSE
├── README.md
├── cliff.toml
├── cmd/
│   └── helm-schema/
│       ├── cli.go
│       ├── main.go
│       ├── main_test.go
│       └── version.go
├── examples/
│   ├── Chart.yaml
│   ├── values.schema.json
│   └── values.yaml
├── go.mod
├── go.sum
├── install-binary.sh
├── pkg/
│   ├── chart/
│   │   ├── chart.go
│   │   ├── chart_test.go
│   │   └── searching/
│   │       └── dependency_charts.go
│   ├── schema/
│   │   ├── annotate.go
│   │   ├── annotate_test.go
│   │   ├── err.go
│   │   ├── err_test.go
│   │   ├── root_schema_test.go
│   │   ├── schema.go
│   │   ├── schema_test.go
│   │   ├── toposort.go
│   │   ├── toposort_test.go
│   │   ├── values_merge.go
│   │   ├── worker.go
│   │   └── worker_test.go
│   └── util/
│       ├── file.go
│       └── file_test.go
├── plugin.yaml
├── renovate.json
├── sign-plugin.sh
├── signing-key.asc
└── tests/
    ├── .gitignore
    ├── charts/
    │   ├── Chart.yaml
    │   ├── ref_input.json
    │   ├── test_annotate_expected.yaml
    │   ├── test_annotate_input.yaml
    │   ├── test_helm_defaults.yaml
    │   ├── test_helm_defaults_expected.schema.json
    │   ├── test_ref.yaml
    │   ├── test_ref_expected.schema.json
    │   ├── test_ref_properties.yaml
    │   ├── test_ref_properties_expected.schema.json
    │   ├── test_ref_toplevel.yaml
    │   ├── test_ref_toplevel_expected.schema.json
    │   ├── test_simple.yaml
    │   └── test_simple_expected.schema.json
    ├── import-values/
    │   ├── child/
    │   │   ├── Chart.yaml
    │   │   └── values.yaml
    │   ├── child-complex/
    │   │   ├── Chart.yaml
    │   │   └── values.yaml
    │   ├── parent/
    │   │   ├── Chart.yaml
    │   │   ├── values.schema.expected.json
    │   │   └── values.yaml
    │   └── parent-complex/
    │       ├── Chart.yaml
    │       ├── values.schema.expected.json
    │       └── values.yaml
    ├── preexisting-schema/
    │   ├── dep-with-schema/
    │   │   ├── Chart.yaml
    │   │   ├── values.schema.json
    │   │   └── values.yaml
    │   └── parent/
    │       ├── Chart.yaml
    │       ├── values.schema.default-expected.json
    │       ├── values.schema.expected.json
    │       └── values.yaml
    ├── run.sh
    └── test-sign-plugin.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.go]
indent_size = 4


================================================
FILE: .github/workflows/build_and_test.yml
================================================
---
name: build and test

on: push

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0
      - name: Set up QEMU
        uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
      - name: Set up Go
        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
        with:
          go-version: ^1.21
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7
        with:
          distribution: goreleaser
          version: latest
          args: release --snapshot --clean
      - name: Upload result
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: helm-schema-tarball
          path: dist/helm-schema_*-next_Linux_x86_64.tar.gz
  test:
    runs-on: ubuntu-latest
    needs: goreleaser
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0
      - name: Download helm-schema
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          name: helm-schema-tarball
          path: .
      - name: Install jq
        run: sudo apt-get update && sudo apt-get install -y jq
      - shell: bash
        run: |-
          tar xf helm-schema_*-next_Linux_x86_64.tar.gz -C tests helm-schema
          cd tests && ./run.sh
  test-sign-plugin-script:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: Set up Helm
        uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5
      - name: Test plugin signing
        run: ./tests/test-sign-plugin.sh


================================================
FILE: .github/workflows/release.yml
================================================
---
name: release

on:
  push:
    tags:
      - "*"

permissions:
  packages: write
  contents: write

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    env:
      HAS_GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY != '' }}
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0
      - name: Set up QEMU
        uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
      - name: Set up Go
        uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
        with:
          go-version: ^1.21
      - name: Generate release notes
        uses: orhun/git-cliff-action@f50e11560dce63f7c33227798f90b924471a88b5 # v4
        with:
          config: cliff.toml
          args: --latest --strip header --verbose
        env:
          OUTPUT: RELEASE_NOTES.md
          GITHUB_REPO: ${{ github.repository }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: Import GPG key for plugin signing
        id: import_gpg
        if: ${{ env.HAS_GPG_KEY == 'true' }}
        uses: crazy-max/ghaction-import-gpg@2dc316deee8e90f13e1a351ab510b4d5bc0c82cd # v7
        with:
          gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
          passphrase: ${{ secrets.GPG_PASSPHRASE }}
      - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7
        with:
          distribution: goreleaser
          version: latest
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint || '' }}
      - name: Sign plugin tarballs for Helm v4 verification
        if: ${{ env.HAS_GPG_KEY == 'true' }}
        env:
          GPG_SIGNING_KEY: ${{ steps.import_gpg.outputs.fingerprint }}
          GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
        run: |
          echo "Signing plugin tarballs with GPG for Helm v4 verification..."
          chmod +x sign-plugin.sh

          # Get version from tag
          VERSION="${GITHUB_REF#refs/tags/}"
          VERSION="${VERSION#v}"  # Remove 'v' prefix if present

          # Sign all tar.gz files (plugin packages)
          for tarball in dist/*.tar.gz; do
            if [ -f "$tarball" ]; then
              echo "Signing: $tarball"
              ./sign-plugin.sh "$VERSION" "$tarball" "$GPG_SIGNING_KEY" || {
                echo "Warning: Failed to sign $tarball"
              }
            fi
          done

          # List created .prov files
          echo "Created provenance files:"
          ls -lh dist/*.prov || echo "No .prov files created"
      - name: Set up Helm v4
        uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5
      - name: Test signed plugin tarballs with Helm v4
        if: ${{ env.HAS_GPG_KEY == 'true' }}
        run: |
          echo "Testing signed plugin tarballs with Helm..."

          # Create keyring from the public key in the repo
          KEYRING=$(mktemp)
          gpg --dearmor < signing-key.asc > "$KEYRING"

          FAILED=0
          for prov_file in dist/*.prov; do
            if [ -f "$prov_file" ]; then
              tarball="${prov_file%.prov}"
              if [ -f "$tarball" ]; then
                echo "Verifying: $tarball"
                helm plugin verify "$tarball" --keyring "$KEYRING" || {
                  echo "FAIL: Verification failed for $tarball"
                  cat "$prov_file"
                  FAILED=1
                }
              fi
            fi
          done

          if [ "$FAILED" -eq 1 ]; then
            exit 1
          fi
      - name: Update GitHub release notes
        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
        with:
          body_path: RELEASE_NOTES.md
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: Upload provenance files to release
        if: ${{ env.HAS_GPG_KEY == 'true' }}
        uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
        with:
          files: dist/*.prov
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .gitignore
================================================
./values.yaml
./values.schema.json

dist/
RELEASE_NOTES.md
.vscode

/helm-schema
/tests/helm-schema


================================================
FILE: .goreleaser.yaml
================================================
---
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj

version: 2

before:
  hooks:
    - go mod tidy
    - go test ./...

builds:
  - main: ./cmd/helm-schema
    env:
      - CGO_ENABLED=0
    goarch:
      - amd64
      - arm
      - arm64
    goarm:
      - "6"
      - "7"
    goos:
      - linux
      - windows
      - darwin

archives:
  - formats: tar.gz
    # this name template makes the OS and Arch compatible with the results of uname.
    name_template: >-
      {{ .ProjectName }}_
      {{- .Version }}_
      {{- title .Os }}_
      {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }}
      {{- if .Arm }}v{{ .Arm }}{{ end }}
    # use zip for windows archives
    format_overrides:
      - goos: windows
        formats: zip
    # Include plugin files for Helm plugin installation
    files:
      - plugin.yaml
      - install-binary.sh
      - README.md
      - LICENSE

checksum:
  name_template: checksums.txt

# Sign artifacts for Helm v4 plugin verification
# The actual .prov file creation happens in GitHub Actions workflow
# after goreleaser completes, using sign-plugin.sh script
# Checksum signing is optional and only happens when GPG_FINGERPRINT is set
signs:
  - cmd: sh
    args:
      - -c
      - |
        if [ -n "${GPG_FINGERPRINT}" ]; then
          gpg --batch --local-user "${GPG_FINGERPRINT}" --output "${signature}" --detach-sign "${artifact}"
        else
          echo "Skipping signing: GPG_FINGERPRINT not set"
          touch "${signature}"
        fi
    signature: "${artifact}.sig"
    artifacts: checksum

snapshot:
  version_template: "{{ .Tag }}-next"

dockers_v2:
  - images:
      - "ghcr.io/dadav/helm-schema"
    tags:
      - "v{{ .Version }}"
      - "{{ if .IsNightly }}nightly{{ end }}"
      - "{{ if not .IsNightly }}latest{{ end }}"
    labels:
      "org.opencontainers.image.created": "{{.Date}}"
      "org.opencontainers.image.authors": dadav
      "org.opencontainers.image.url": "{{.GitURL}}"
      "org.opencontainers.image.title": "{{.ProjectName}}"
      "org.opencontainers.image.revision": "{{.FullCommit}}"
      "org.opencontainers.image.version": "{{.Version}}"

changelog:
  sort: asc
  filters:
    include:
      - "^feat:"
      - "^fix:"


================================================
FILE: .pre-commit-config.yaml
================================================
---
repos:
  - repo: https://github.com/dadav/helm-schema
    rev: 0.22.0
    hooks:
      - id: helm-schema
        # for all available options: helm-schema -h
        args:
          # directory to search recursively within for charts
          - --chart-search-root=.

          # don't analyze dependencies
          - --no-dependencies

          # add references to values file if not exist
          - --add-schema-reference

          # list of fields to skip from being created by default
          # e.g. generate a relatively permissive schema
          # - "--skip-auto-generation=required,additionalProperties"


================================================
FILE: .pre-commit-hooks.yaml
================================================
---
- id: helm-schema
  description: Uses helm-schema to create a jsonschema.
  entry: helm-schema
  files: (Chart|values)\.yaml$
  language: golang
  name: Generate jsonschema
  require_serial: true
- id: helm-schema-system
  description: Uses pre-installed helm-schema to create a jsonschema.
  entry: helm-schema
  files: (Chart|values)\.yaml$
  language: system
  name: Generate jsonschema
  require_serial: true
- id: helm-schema-container
  args: []
  description: Uses the container image of 'helm-schema' to create schema from the Helm chart's 'values.yaml' file, and inserts the result into a corresponding 'values.schema.json' file.
  entry: ghcr.io/dadav/helm-schema:latest
  files: values.yaml
  language: docker_image
  name: Helm Schema Container
  require_serial: true


================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

`helm-schema` is a Go tool that generates JSON Schema (Draft 7) files for Helm chart values. It traverses directories to find `Chart.yaml` files, reads associated `values.yaml` files, parses special `@schema` annotations in comments, and generates `values.schema.json` files that enable IDE auto-completion and validation for Helm values.

## Build and Test Commands

### Build

```bash
# Build the binary
go build -o helm-schema ./cmd/helm-schema

# Build with goreleaser (for releases)
goreleaser release --snapshot --clean
```

### Test

```bash
# Run all tests
go test ./...

# Run tests for a specific package
go test ./pkg/schema
go test ./pkg/chart

# Run a specific test
go test ./pkg/schema -run TestTopoSort

# Run tests with verbose output
go test -v ./...

# Integration tests (requires helm-schema binary in tests/)
cd tests && ./run.sh
```

### Linting and Formatting

```bash
# Format code
go fmt ./...

# Tidy dependencies
go mod tidy
```

## Code Architecture

### High-Level Flow

1. **Chart Discovery** (`pkg/chart/searching/`): Recursively searches for `Chart.yaml` files starting from a root directory. Also extracts `.tgz` chart archives if found.

2. **Parallel Processing** (`cmd/helm-schema/main.go`): Uses a worker pool (2x CPU cores) to process charts concurrently. Each worker receives chart paths via a channel.

3. **Schema Generation** (`pkg/schema/worker.go`): For each chart, reads the `values.yaml` file and parses YAML with annotations to build a JSON Schema object.

4. **Annotation Parsing** (`pkg/schema/schema.go`): Parses `# @schema` and `# @schema.root` comment blocks to extract JSON Schema properties (type, description, enum, pattern, etc.).

5. **Dependency Resolution** (`cmd/helm-schema/main.go`): After all charts are processed, topologically sorts them by dependencies and merges dependency schemas into parent charts. Library charts (type: library) have their properties merged at the top level.

6. **Output** (`cmd/helm-schema/main.go`): Writes `values.schema.json` files to each chart directory.

7. **Annotation Mode** (`pkg/schema/annotate.go`): With `--annotate` / `-A` flag, writes inferred `@schema` type annotation blocks into `values.yaml` files for keys that don't already have them. This is a separate execution mode that modifies values files instead of generating JSON schema. Keys that already have `@schema` blocks are skipped.

### Key Components

#### Schema Parsing (`pkg/schema/schema.go`)

- **`ParseValues()`**: Main entry point that parses a values.yaml file and returns a Schema
- **`parseYamlNode()`**: Recursively traverses YAML nodes, extracting schema annotations and inferring types
- **Annotation blocks**: Comments between `# @schema` markers are parsed as YAML to extract JSON Schema properties
- **Root annotations**: Comments between `# @schema.root` markers apply to the root schema object itself
- **Type inference**: If no type is specified, the tool infers it from YAML tags (!!str, !!int, !!bool, etc.)

#### Worker Pattern (`pkg/schema/worker.go`)

- Workers pull chart paths from a channel and process them independently
- Each worker:
  1. Reads Chart.yaml
  2. For schema generation, finds all configured values files that exist for the chart and merges them in CLI order
  3. Parses merged values into a Schema
  4. Sends Result to results channel

- When multiple values files are present, later files override earlier files using Helm-style nested map merge precedence.
- `--annotate` and `--add-schema-reference` still operate on the first matching values file only; they do not merge multiple files.

#### Dependency Graph (`pkg/schema/toposort.go`)

- **TopoSort()**: Uses DFS-based topological sorting to ensure dependencies are processed before dependents
- Detects circular dependencies and can either fail or warn based on `allowCircular` flag
- Returns charts in dependency order (dependencies first, parents last)

#### Chart Models (`pkg/chart/chart.go`)

- **ChartFile**: Represents Chart.yaml structure
- **Dependency**: Represents a chart dependency with name, version, alias, condition

#### Schema Merging (in `main.go`)

- Regular dependencies: Nested under dependency name (or alias) in parent schema
- Library charts: Properties merged directly into parent schema at top level
- Import-values: Properties from dependency's `exports` section (or custom paths) merged into parent at specified location
- Conditional dependencies: If a dependency has a `condition` field, the corresponding boolean property is auto-created in the dependency's schema
- Skip validation flag (`-m`): Can disable strict validation for dependencies by setting `additionalProperties: true`

#### Import-Values Processing (in `main.go`)

The `processImportValues()` function handles Helm's `import-values` directive:

- **Simple form** (`import-values: [defaults]`): Imports from `exports.defaults` in dependency to parent's root
- **Complex form** (`import-values: [{child: "path", parent: "path"}]`): Explicit source and target paths
- Properties are merged using `mergeSchemaProperties()`, which skips "global" and warns on conflicts
- When import-values is used on a non-library dependency, the dependency is NOT auto-nested (user controls what's imported)

#### Schema Path Navigation (`pkg/schema/schema.go`)

- **`GetPropertyAtPath(path string)`**: Navigates dot-separated paths (e.g., "exports.defaults") and returns the schema at that location, or nil if not found
- **`SetPropertyAtPath(path string)`**: Creates intermediate object schemas as needed and returns the schema at the target path

### Important Patterns

1. **BoolOrArrayOfString**: The `required` field in JSON Schema can be either a boolean or an array of strings. This custom type handles both cases during marshaling/unmarshaling.

2. **SchemaOrBool**: Some JSON Schema fields like `additionalProperties` can be either a boolean or a Schema object. This is represented as `interface{}`.

3. **Annotation Comments**: The tool looks for comments in specific formats:
   - `# @schema` / `# @schema` blocks for field-level annotations
   - `# @schema.root` / `# @schema.root` blocks for root-level annotations
   - Comments outside these blocks become descriptions (unless `description` is explicitly set)

4. **Helm-docs compatibility**: With `-p` flag, parses `-- helm-docs description` and `@default` annotations from helm-docs format.
   - Helm-docs type `tpl` is mapped to JSON Schema `string` in `helmDocsTypeToSchemaType`.

## Testing Strategy

- **Unit tests**: Each package has `*_test.go` files testing individual functions
- **Integration tests**: `tests/run.sh` compares generated schemas against expected outputs
- **Test files**: `tests/test_*.yaml` are input values files, `tests/test_*_expected.schema.json` are expected outputs

## Plugin Verification (Helm v4)

The project implements Helm v4 plugin verification through GPG signing:

### Signing Infrastructure

1. **sign-plugin.sh**: Script that creates `.prov` (provenance) files for plugin tarballs
   - Takes version, tarball path, and GPG key as arguments
   - Creates a signed provenance file containing metadata and SHA256 hash
   - Uses GPG to sign the provenance

2. **GitHub Actions Workflow**: `.github/workflows/release.yml`
   - Generates release notes with `orhun/git-cliff-action@v4` using `cliff.toml`
   - Imports GPG private key from secrets (`GPG_PRIVATE_KEY`, `GPG_PASSPHRASE`)
   - Runs goreleaser to build and package binaries
   - Updates the GitHub release body from `RELEASE_NOTES.md`
   - Signs all `.tar.gz` files with `sign-plugin.sh`
   - Uploads `.prov` files to GitHub releases

3. **GoReleaser Config**: `.goreleaser.yaml`
   - Archives include plugin files: `plugin.yaml`, `install-binary.sh`, `README.md`, `LICENSE`
   - Configured to sign checksums with GPG

4. **git-cliff Config**: `cliff.toml`
   - Groups conventional commits into release note sections
   - Uses GitHub metadata to link pull requests in generated release notes
   - Generates only the latest tagged release notes in CI via `--latest --strip header`

### Setup for Maintainers

- See `.github/SETUP_SIGNING.md` for initial GPG key setup
- Public key should be in `signing-key.asc` (currently a template)
- Key details must be updated in `VERIFICATION.md`

### Verification Process

Users can verify plugins with:

```bash
helm plugin install <tarball> --verify
helm plugin verify schema
```

## Supported JSON Schema Draft 7 Keywords

The Schema struct (`pkg/schema/schema.go`) supports the following JSON Schema Draft 7 keywords:

### Core Keywords

- `$schema`, `$id`, `$ref`, `$comment`
- `type` (single type or array of types)
- `title`, `description`
- `default`, `examples`
- `definitions` (also supports `$defs` from Draft 2019-09+ - automatically converted to `definitions`)

### Validation Keywords

#### Numeric (number, integer)

- `minimum`, `maximum` (float64 - supports decimal values like `1.5`)
- `exclusiveMinimum`, `exclusiveMaximum` (float64)
- `multipleOf` (float64 - supports `0.1`, `0.01`, etc.)

#### String

- `minLength`, `maxLength`
- `pattern` (regex pattern)
- `format` (date-time, email, uri, ipv4, ipv6, uuid, etc.)
- `contentEncoding`, `contentMediaType`

#### Array

- `items` (single schema or handled via anyOf for arrays)
- `additionalItems` (boolean or schema)
- `minItems`, `maxItems`
- `uniqueItems`
- `contains`

#### Object

- `properties`, `patternProperties`
- `additionalProperties` (boolean or schema)
- `required` (boolean or array of strings)
- `minProperties`, `maxProperties`
- `propertyNames`
- `dependencies`

### Composition Keywords

- `allOf`, `anyOf`, `oneOf`, `not`
- `if`, `then`, `else`

### Annotation Keywords

- `deprecated`, `readOnly`, `writeOnly`
- `enum`, `const`
- `const-from-value` copies the YAML value into the generated `const` keyword and must not be combined with explicit `const`

### Custom Annotations

- Any key prefixed with `x-` is treated as a custom annotation

## Documentation Notes

- README plugin verification examples should use a `vX.Y.Z` placeholder to avoid version drift.
- GPG public key fingerprint (from `signing-key.asc`) is `806F 70D2 5667 D42A AE4E 07CE F587 0796 9D0F BFA5`; key ID is `F58707969D0FBFA5`.

## Validation Behavior

The schema validation (`Validate()` method) performs type-specific constraint checks:

- Numeric constraints (`minimum`, `maximum`, etc.) require `type: number` or `type: integer`
- String constraints (`minLength`, `maxLength`, `pattern`, `format`, `contentEncoding`) require `type: string`
- Array constraints (`items`, `minItems`, `maxItems`, `contains`, `additionalItems`) require `type: array`
- Object constraints (`minProperties`, `maxProperties`, `propertyNames`, `additionalProperties`) require `type: object`

Some keywords like `uniqueItems` are accepted on any type per the JSON Schema spec (keywords are ignored if the type doesn't match).

## Common Gotchas

1. **Draft 7 limitation**: The tool uses JSON Schema Draft 7 because Helm's validation library only supports that version. When referencing external schemas that use `$defs` (Draft 2019-09+), they are automatically converted to `definitions` and `$ref` paths are rewritten from `#/$defs/` to `#/definitions/`.

2. **Root annotations placement**: `@schema.root` blocks must be at the top of values.yaml with no blank lines after (unless using `-s` flag).

3. **Dependency schema immutability**: You cannot modify a dependency's schema using annotations in the parent chart's values.yaml. The schema comes from the dependency's own values.yaml.

4. **Library chart merging**: When a library chart property name conflicts with a parent property, the parent takes precedence (with a warning logged).

5. **Import-values behavior**: When `import-values` is used on a non-library dependency, the dependency's full schema is NOT auto-nested under its name. Only explicitly imported properties appear in the parent schema. This matches Helm's behavior where import-values gives the user explicit control over what's imported.

6. **Comment parsing**: By default, descriptions are cut at the first empty line in comments. Use `-s` to keep full comments.

7. **Plugin signing**: Signing only works if GPG secrets are configured in GitHub. Missing secrets cause signing steps to be skipped gracefully.

8. **Dependency schema regeneration**: By default every discovered chart — including those declared as dependencies of another chart — has its `values.schema.json` regenerated from its own `values.yaml`. Pass `--keep-existing-dep-schemas` (`-K`) to instead reuse a dependency chart's pre-existing `values.schema.json` (useful for third-party charts pulled via `helm dep up`, so their rich constraints and `x-*` annotations are preserved). When the flag is set, such dependency schemas are merged into the parent as-is and are not overwritten on disk.


================================================
FILE: Dockerfile
================================================
FROM alpine:3.23@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11
ARG TARGETPLATFORM
RUN adduser -k /dev/null -u 10001 -D helm-schema \
  && chgrp 0 /home/helm-schema \
  && chmod -R g+rwX /home/helm-schema
COPY $TARGETPLATFORM/helm-schema /
USER 10001
VOLUME [ "/home/helm-schema" ]
WORKDIR /home/helm-schema
ENTRYPOINT ["/helm-schema"]


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 dadav

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# helm-schema

<p align="center">
  <img src="images/logo.png" width="400" />
  <br />
  <a href="https://github.com/dadav/helm-schema/releases"><img src="https://img.shields.io/github/release/dadav/helm-schema.svg" alt="Latest Release"></a>
  <a href="https://pkg.go.dev/github.com/dadav/helm-schema?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="Go Docs"></a>
  <a href="https://github.com/dadav/helm-schema/actions"><img src="https://img.shields.io/github/actions/workflow/status/dadav/helm-schema/build_and_test.yml" alt="Build Status"></a>
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="MIT LICENSE"></a>
  <a href="https://github.com/pre-commit/pre-commit"><img src="https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit" alt="pre-commit" style="max-width:100%;"></a>
  <a href="https://goreportcard.com/badge/github.com/dadav/helm-schema"><img src="https://goreportcard.com/badge/github.com/dadav/helm-schema" alt="Go Report"></a>
</p>

<p align="center">This tool tries to help you to easily create some nice <a href="https://json-schema.org/" target="_blank"><strong>JSON schema</strong></a> for your helm chart.</p>

By default it will traverse the current directory and look for `Chart.yaml` files.
For every file, helm-schema will try to find one of the given value filenames.
The first files found will be read and a jsonschema will be created.
For every dependency defined in the `Chart.yaml` file, a reference to the dependencies JSON schema
will be created.

> [!NOTE]
> The tool uses `jsonschema` Draft 7, because the library helm uses only supports that version.

## Installation

Via `go` install:

```sh
go install github.com/dadav/helm-schema/cmd/helm-schema@latest
```

From `aur`:

```sh
paru -S helm-schema
```

Via `podman/docker`:

```sh
podman run --rm -v $PWD:/home/helm-schema ghcr.io/dadav/helm-schema:latest
```

As `helm plugin`:

```sh
helm plugin install https://github.com/dadav/helm-schema
```

### Plugin Verification (Helm v4+)

Helm v4 introduced plugin verification for enhanced security. All helm-schema releases are signed with GPG and include provenance files (`.prov`) for verification.

**Automatic Verification (Recommended)**

Helm v4 verifies plugin signatures by default:

```sh
# Install from a specific release with automatic verification
helm plugin install https://github.com/dadav/helm-schema/releases/download/vX.Y.Z/helm-schema_X.Y.Z_Linux_x86_64.tar.gz
```

**Manual Verification**

Before installing, import the signing key:

```sh
# Import the public signing key (from file)
gpg --import signing-key.asc

# Or import by key ID (last 16 hex chars of the fingerprint)
gpg --keyserver keyserver.ubuntu.com --recv-keys F58707969D0FBFA5

# Verify the imported key fingerprint (expect: 806F 70D2 5667 D42A AE4E 07CE F587 0796 9D0F BFA5)
gpg --fingerprint F58707969D0FBFA5

# Export from kdx and save in old gpg format
gpg --export F58707969D0FBFA5 > ~/.gnupg/pubring.gpg

# Install with explicit verification
helm plugin install https://github.com/dadav/helm-schema/releases/download/vX.Y.Z/helm-schema_X.Y.Z_Linux_x86_64.tar.gz --verify
```

**Verify Installed Plugin**

```sh
# Verify an already installed plugin
helm plugin verify schema
```

> [!NOTE]
> Plugin verification requires Helm v4 or later. If using Helm v3, signatures will be ignored.

## Usage

### Pre-commit hook

If you want to automatically generate a new `values.schema.json` if you change the `values.yaml`
file, you can do the following:

1. Install [`pre-commit`](https://pre-commit.com/#install)
2. Copy the [`.pre-commit-config.yaml`](./.pre-commit-config.yaml) to your helm chart repository.
3. Then run these commands:

```sh
pre-commit install
pre-commit install-hooks
```

### Running the binary directly

You can also just run the binary yourself:

```sh
helm-schema
```

### Options

The binary has the following options:

```sh
Flags:
  -A, --annotate                               "write inferred @schema type blocks into the first matching values file instead of generating schema"
  -r, --add-schema-reference                   "add reference to schema in values.yaml if not found"
  -w, --allow-circular-dependencies            "allow circular dependencies between charts (will log a warning instead of failing)"
  -a, --append-newline                         "append newline to generated jsonschema at the end of the file"
  -c, --chart-search-root string               "directory to search recursively within for charts (default ".")"
  -i, --dependencies-filter strings            "only generate schema for specified dependencies (comma-separated list of dependency names)"
  -g, --dont-add-global                        "don't auto add global property"
  -x, --dont-strip-helm-docs-prefix            "disable the removal of the helm-docs prefix (--)"
  -d, --dry-run                                "don't actually create files just print to stdout passed"
  -p, --helm-docs-compatibility-mode           "parse and use helm-docs comments"
  -h, --help                                   "help for helm-schema"
  -K, --keep-existing-dep-schemas              "use dependency charts' pre-existing values.schema.json instead of regenerating from values.yaml"
  -s, --keep-full-comment                      "keep the whole leading comment (default: cut at empty line)"
  -l, --log-level string                       "level of logs that should be printed, one of (panic, fatal, error, warning, info, debug, trace) (default "info")"
  -n, --no-dependencies                        "skip dependency charts: don't merge them into parents and don't generate their schemas"
  -o, --output-file string                     "jsonschema file path relative to each chart directory to which jsonschema will be written (default 'values.schema.json')"
  -m, --skip-dependencies-schema-validation    "skip schema validation for dependencies by setting additionalProperties to true and removing from required"
  -f, --value-files strings                    "filenames to look for chart values; schema generation merges all matches in the order provided (default [values.yaml])"
  -k, --skip-auto-generation strings           "skip the auto generation for these fields (default [])"
  -u, --uncomment                              "consider yaml which is commented out"
  -v, --version                                "version for helm-schema"
```

For schema generation, `helm-schema` checks each `--value-files` entry for the chart, keeps the ones that exist, and merges them in the order provided. Later files take precedence over earlier files, following Helm's `-f/--values` behavior.

`--annotate` does not merge multiple files. It only annotates the first matching values file.

`--add-schema-reference` also targets the first matching values file.

### Annotate mode

Use `--annotate` to add inferred `# @schema` type blocks to a values file instead of generating `values.schema.json`.

- Keys that already have `@schema` annotations are left unchanged.
- With `-d, --dry-run`, the annotated file is printed to stdout instead of being written back.
- When multiple `--value-files` entries are configured, annotate mode uses only the first matching file.

## Annotations

The `jsonschema` must be between two entries of `# @schema` :

```yaml
# @schema
# my: annotation
# @schema
# you can add comment here as well
foo: bar
```

> [!WARNING]
> It must be written just above the key you want to annotate.

> [!NOTE]
> If you don't use the `properties` option on hashes/objects or don't use `items` on arrays, it will be parsed from the values and their annotations instead.

### Root-level annotations

You can apply schema annotations to the root schema object itself using `# @schema.root`:

```yaml
# @schema.root
# title: My Chart Values
# description: Configuration values for my Helm chart
# x-custom-field: custom-value
# @schema.root
# @schema
# enum: [dev, staging, prod]
# @schema
# Example description foo baz
stage: dev
```

> [!NOTE]
> The `@schema.root` block must be placed before the first key in your `values.yaml` file, without blank lines after it (unless you use the `-s` flag to keep full comments).

### Available annotations

<!-- prettier-ignore -->
| Key| Description | Values |
|-|-|-|
| [`type`](#type) | Defines the [jsonschema-type](https://json-schema.org/understanding-json-schema/reference/type.html) of the object. Multiple values are supported (e.g. `[string, integer]`) as a shortcut to `anyOf` | `object`, `array`, `string`, `number`, `integer`, `boolean` or `null` |
| [`title`](#title) | Defines the [title field](https://json-schema.org/understanding-json-schema/reference/generic.html?highlight=title) of the object | Defaults to the key itself |
| [`description`](#description) | Defines the [description field](https://json-schema.org/understanding-json-schema/reference/generic.html?highlight=description) of the object. | Defaults to the comments just above or below the `@schema` annotations block |
| [`default`](#default) | Sets the default value and will be displayed first on the users IDE| Takes a `string` |
| [`properties`](#properties) | Contains a map with keys as property names and values as schema | Takes an `object` |
| [`pattern`](#pattern) | Regex pattern to test the value | Takes an `string` |
| [`format`](#format) | The [format keyword](https://json-schema.org/understanding-json-schema/reference/string.html#format) allows for basic semantic identification of certain kinds of string values | Takes a [keyword](https://json-schema.org/understanding-json-schema/reference/string.html#format) |
| [`required`](#required) | Adds the key to the required items | `true` or `false` or `array` |
| [`deprecated`](#deprecated) | Marks the option as deprecated | `true` or `false` |
| [`items`](#items) | Contains the schema that describes the possible array items | Takes an `object` |
| [`enum`](#enum) | Multiple allowed values. Accepts an array of `string` | Takes an `array` |
| [`const`](#const) | Single allowed value | Takes a `string`|
| [`examples`](#examples) | Some examples you can provide for the end user | Takes an `array` |
| [`minimum`](#minimum) | Minimum value. Can't be used with `exclusiveMinimum` | Takes a `number` (integer or float). Must be smaller than `maximum` or `exclusiveMaximum` (if used) |
| [`exclusiveMinimum`](#exclusiveminimum) | Exclusive minimum. Can't be used with `minimum` | Takes a `number` (integer or float). Must be smaller than `maximum` or `exclusiveMaximum` (if used) |
| [`maximum`](#maximum) | Maximum value. Can't be used with `exclusiveMaximum` | Takes a `number` (integer or float). Must be bigger than `minimum` or `exclusiveMinimum` (if used) |
| [`exclusiveMaximum`](#exclusivemaximum) | Exclusive maximum value. Can't be used with `maximum` | Takes a `number` (integer or float). Must be bigger than `minimum` or `exclusiveMinimum` (if used) |
| [`multipleOf`](#multipleof) | The yaml-value must be a multiple of. For example: If you set this to 0.1, allowed values would be 0, 0.1, 0.2... | Takes a `number` (integer or float, must be > 0) |
| [`additionalProperties`](#additionalproperties) | Allow additional keys in maps. Useful if you want to use for example `additionalAnnotations`, which will be filled with keys that the `jsonschema` can't know| Defaults to `false` if the map is not an empty map. Takes a schema or boolean value |
| [`patternProperties`](#patternproperties) | Contains a map which maps schemas to pattern. If properties match the patterns, the given schema is applied| Takes an `object` |
| [`anyOf`](#anyof) | Accepts an array of schemas. None or one must apply | Takes an `array` |
| [`oneOf`](#oneof) | Accepts an array of schemas. One or more must apply | Takes an `array` |
| [`allOf`](#allof) | Accepts an array of schemas. All must apply| Takes an `array` |
| [`not`](#not) | A schema that must not be matched. | Takes an `object` |
| [`if/then/else`](#ifthenelse) | `if` the given schema applies, `then` also apply the given schema or `else` the other schema| Takes an `object` |
| [`$ref`](#ref) | Accepts an URI to a valid `jsonschema`. Extend the schema for the current key | Takes an URI (or relative file) |
| [`minLength`](#minlength) | Minimum string length. | Takes an `integer`. Must be smaller or equal than `maxLength` (if used) |
| [`maxLength`](#maxlength) | Maximum string length. | Takes an `integer`. Must be greater or equal than `minLength` (if used) |
| [`minItems`](#minItems) | Minimum length of an array. | Takes an `integer`. Must be smaller or equal than `maxItems` (if used) |
| [`maxItems`](#maxItems) | Maximum length of an array. | Takes an `integer`. Must be greater or equal than `minItems` (if used) |
| [`contains`](#contains) | Array must contain at least one item matching this schema | Takes a schema `object` |
| [`additionalItems`](#additionalItems) | Schema for array items beyond those defined in `items` tuple | Takes a `boolean` or schema `object` |
| [`minProperties`](#minProperties) | Minimum number of properties in an object | Takes an `integer` >= 0 |
| [`maxProperties`](#maxProperties) | Maximum number of properties in an object | Takes an `integer` >= 0 |
| [`propertyNames`](#propertyNames) | Schema that all property names must match | Takes a schema `object` |
| [`dependencies`](#dependencies) | Property dependencies (presence of one property requires others) | Takes an `object` mapping property names to arrays or schemas |
| [`definitions`](#definitions) | Reusable schema definitions for use with `$ref`. Also supports `$defs` from newer JSON Schema drafts (automatically converted) | Takes an `object` mapping names to schemas |
| [`$comment`](#comment) | Comment for schema maintainers (not shown to end users) | Takes a `string` |
| [`contentEncoding`](#contentEncoding) | Encoding for string content (e.g., base64) | Takes a `string` |
| [`contentMediaType`](#contentMediaType) | MIME type for string content | Takes a `string` |

## Validation & completion

To take advantage of the generated `values.schema.json`, you can use it within your IDE through a plugin supporting the `yaml-language-server` annotation (e.g. [VSCode - YAML](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml))

You'll have to place this line at the top of your `values.yaml` (`$schema=<path-or-url-to-your-schema>`) :

```yaml
# vim: set ft=yaml:
# yaml-language-server: $schema=values.schema.json

# @schema
# required: true
# @schema
# -- This is an example description
foo: bar
```

You can use the `-r` flag to make sure this line exists.

> [!NOTE]
> You can also point to an online available schema, if you upload a version of yours and want other to be able to implement it.
>
> `yaml-language-server: $schema=https://example.org/my-json-schema.json`
>
> e.g. from github `https://raw.githubusercontent.com/<user>/<repo>/main/values.schema.json`

### helm-docs

If you're using [`helm-docs`](https://github.com/norwoodj/helm-docs), then you can combine both annotations and use both pre-commit hooks to automatically generate your documentation (e.g. `README.md`) alongside your `values.schema.json`.

If not provided, `title` will be the key and the `description` will be parsed from the `helm-docs` formatted comment.

```yaml
# @schema
# type: array
# @schema
# -- helm-docs description here
foo: []
```

If you use `-p`/`--helm-docs-compatibility-mode` flags, the `@default`, `(type)` annotations and helm-docs descriptions
are used if detected.

> [!NOTE]
> Make sure to place the `@schema` annotations **before** the actual key description to avoid having it in your `helm-docs` generated table

## Dependencies

By default, `helm-schema` will try to also create the schemas for the dependencies in their respective chart directory. These schemas will be merged as properties in the main schema, but the `requiredProperties` field will be nullified, otherwise you would have to always overwrite all the required fields.

If you don't want to generate `jsonschema` for chart dependencies, you can use the `-n, --no-dependencies` option to only generate the `values.schema.json` for your parent chart(s). With this flag, any discovered chart that is declared as a dependency of another discovered chart is skipped entirely — the dependency is not merged into its parent and its own `values.schema.json` is not generated.

### Reusing a Dependency's Pre-existing Schema

By default, `helm-schema` regenerates `values.schema.json` for every discovered chart — including subcharts that already ship with a hand-written `values.schema.json`. If you instead want to preserve a dependency's shipped schema (typical for third-party charts pulled via `helm dep up` that carry rich constraints such as `minimum`, `pattern`, `format`, or custom `x-*` annotations), pass `-K, --keep-existing-dep-schemas`:

```sh
helm-schema -K
```

When this flag is set:

1. A dependency chart's pre-existing `values.schema.json` is used as-is and merged into the parent.
2. That dependency's schema file is not overwritten on disk.

Without the flag, every discovered chart's schema is regenerated from its `values.yaml`.

### Library Charts

When a dependency has `type: library` in its `Chart.yaml`, `helm-schema` will merge its schema properties directly into the parent chart's schema at the top level, rather than nesting them under the dependency name. This reflects how [Helm library charts](https://helm.sh/docs/topics/library_charts/) work in practice, where the values scope is identical to the parent chart.

For example, if you have a library chart named `common` with properties `environment` and `region`, these will appear at the top level of the parent schema alongside the parent's own properties, rather than under a `common` key.

**Note:** If a library chart property has the same name as a property already defined in the parent chart, the parent's property takes precedence and a warning will be logged.

### Skip Dependency Schema Validation

By default, when dependency schemas are merged into the parent chart schema, they inherit strict validation rules. This means that if you add unknown keys at the top level of a dependency's values (e.g., `subchart.unknownKey`), validation may not fail as expected.

If you want to allow additional properties in dependency schemas and ensure they are not required, you can use the `-m, --skip-dependencies-schema-validation` flag. This will:

1. Set `additionalProperties: true` for all dependency schemas in the parent chart
2. Remove dependency names from the parent chart's required properties list

Example usage:

```sh
helm-schema -m
```

This is useful when you have umbrella charts with multiple dependencies and want to allow flexibility in overriding dependency values without strict schema validation.

### Handling Circular Dependencies

In some scenarios, you may have charts that reference each other to share values, creating circular dependencies. For example:

- A cert-manager chart depends on a grafana chart to get the instance name for creating dashboards
- The grafana chart depends on the cert-manager chart to get the ACME issuer value

By default, `helm-schema` will detect this as a circular dependency and log a warning. The tool will still continue processing, but the results may not be sorted in dependency order.

If you want to explicitly allow circular dependencies and acknowledge this behavior, you can use the `-w, --allow-circular-dependencies` flag:

```sh
helm-schema -w
```

When this flag is enabled, circular dependencies are treated as warnings rather than errors, and the charts are processed in their original discovery order instead of topologically sorted order.

**Note:** This is primarily useful when charts have cross-dependencies purely for value sharing, not for actual build order dependencies.

## Limitations

You can't change the `jsonschema` for dependencies by using `@schema` annotations on dependency config values. For example:

```yaml
# foo is a dependency chart
foo:
  # You can't change the schema here, this has no effect.
  # @schema
  # type: number
  # @schema
  bar: 1
```

## Examples

Some annotation examples you may want to use, to help you get started!

> [!NOTE]
> See how the schema behaves with live examples : [values.yaml](./examples/values.yaml)

Below a snippet to test it out, with the current options `helm-schema` will not analyze dependencies (`-n`) and will omit the `additionalProperties` (`-k`, when not explicitly defined) in the generated schema. It will start looking for `Chart.yaml` and `values.yaml` files in `examples/` (`-c`)

```sh
cd examples
helm-schema -n -k additionalProperties

# or

helm-schema -c examples -n -k additionalProperties
```

If you'd like to use `helm-schema` on your chart dependencies as well, you have to build and unpack them before. You'll avoid the "missing dependency" error message.

```sh
# go where your Chart.lock/yaml is located
cd <chart-name>

# build dependencies and untar them
helm dep build
ls charts/*.tgz |xargs -n1 tar -C charts/ -xzf
```

#### `type`

If `type` isn't specified, current value type will be used.

```yaml
# Will be parsed as 'string'
# @schema
# title: Some title
# description: Some description
# @schema
name: foo

# Will be parsed as 'boolean'
# @schema
# type: boolean
# @schema
enabled: true

# You can define multiple types as an array.
# @schema
# type: [string, integer]
# minimum: 0
# @schema
cpu: 1
```

#### Root schema annotations

Apply schema annotations to the root document itself. This is especially useful for setting the `additionalProperties` on the root level of the schema.

```yaml
# @schema.root
# additionalProperties: true
# @schema.root
# Main application settings
app:
  # @schema
  # type: string
  # @schema
  name: my-app
  # @schema
  # type: boolean
  # @schema
  enabled: true
```

#### `title`

By default, the `title` will be parsed from the key name. If the key is `foo`, then `title: foo`.

```yaml
# Define a custom title for the key
# @schema
# title: My custom title for 'foo'
# @schema
bar: foo
```

#### `description`

You can provide the `description` through its property or let it be parsed from your comments. If `description` is provided, the comments will not be parsed as description.

If you're implementing it alongside `helm-docs`, read [this](#helm-docs) to do it correctly.

```yaml
# This text will be used as description.
# @schema
# type: integer
# minimum: 1
# @schema
replica: 1

# @schema
# type: integer
# minimum: 1
# @schema
# This text will be used as description.
replica: 1

# @schema
# type: integer
# minimum: 1
# description: This text will be used as description.
# @schema
# And not this one
replica: 1
```

#### `default`

Help users when using their IDE to quickly retrieve the `default` value, for example through <kbd>CTRL+SPACE</kbd>.

```yaml
# @schema
# default: standalone
# enum: [standalone,cluster]
# @schema
architecture: ""

# @schema
# type: boolean
# default: true
# @schema
enabled: true
```

#### `properties`

Allows user to define valid keys without defining them yet. Give the user an insight of the possible properties, their types and description.

By default, `title` for the keys defined under `properties` will inherit from the main key (e.g. here `title: env`). You need to provide `title` explicitly if you want to change it.

```yaml
# @schema
# properties:
#   CONFIG_PATH:
#     title: CONFIG_PATH
#     type: string
#     description: The local path to the service configuration file
#   ADMIN_EMAIL:
#     title: ADMIN_EMAIL
#     type: string
#     format: idn-email
#   API_URL:
#     type: string
#     format: idn-hostname
#     description: Title will be 'env' as we do not specify it here
# @schema
# -- Environment variables. If you want to provide auto-completion to the user
env: {}
```

#### `pattern`

Pattern that'll be used to test the value.

```yaml
# @schema
# pattern: ^api-key
# @schema
# The value have to start with the 'api-key-' prefix
apiKey: "api-key-xxxxx"
```

#### `format`

Known formats that the value must match. Formats available at [JSON Schema - Formats](https://json-schema.org/understanding-json-schema/reference/string.html#format).

```yaml
# @schema
# format: idn-email
# @schema
# Requires a valid email format
email: foo@example.org
```

#### `required`

By default every property is a required property, you can disable this with `required: false` for a single key. You can also invert this behaviour with the option `helm-schema -k required`, now every property is an optional one.

```yaml
# @schema
# required: false
# @schema
altName: foo
```

It's also possible to define an array of required properties on the parent.

```yaml
# @schema
# required: [foo]
# @schema
altName:
  foo: bar
```

#### `deprecated`

Let the user know if the key is deprecated, hence should be avoided.

```yaml
# @schema
# deprecated: true
# @schema
secret: foo
```

#### `items`

If you want to specify a schema for possible array values without using a default value. E.g. to define the structure of the hosts definition in an k8s ingress resource.

```yaml
# @schema
# type: array
# items:
#   type: object
#   properties:
#     host:
#       type: object
#       properties:
#         url:
#           type: string
#           format: idn-hostname
# @schema
# Will give auto-completion for the below structure
# hosts:
#  - name:
#      url: my.example.org
hosts: []
```

#### `enum`

Allows user to define available values for a given key. Validation will fail and error shown if you try to put another value.

```yaml
# @schema
# enum:
# - application
# - controller
# - api
# @schema
# Only those three values are accepted
type: application

# @schema
# type: array
# items:
#   enum: [api,frontend,backend,microservice,teamA,teamB,us-west-1,us-west-2]
# @schema
# For each array index, only one of those values are accepted
tags:
  - "api"
  - "teamA"
  - "us-west-2"
```

#### `const`

Defines a constant value which shouldn't be changed.

```yaml
# @schema
# const: maintainer@example.org
# @schema
maintainer: maintainer@example.org
```

#### `const-from-value`

Copies the YAML value into the generated JSON Schema `const` without duplicating the payload in the annotation block.

```yaml
# @schema
# const-from-value: true
# @schema
message: |
  long message with {{ .gotemplate }}
```

#### `examples`

Provides example values to the user when hovering the key in IDE, or by auto-completion mechanism.

```yaml
# @schema
# format: ipv4
# examples: [192.168.0.1]
# @schema
clusterIP: ""

# @schema
# properties:
#   CONFIG_PATH:
#     type: string
#     description: The local path to the service configuration file
#     examples: [/path/to/config]
#   ADMIN_EMAIL:
#     type: string
#     format: idn-email
#     examples: [admin@example.org]
#   API_URL:
#     type: string
#     format: idn-hostname
#     examples: [https://api.example.org]
# @schema
# -- Provide auto-completion and examples to the user
env: {}
```

#### `minimum`

The value have to be above or equal the given `integer`.

```yaml
# @schema
# minimum: 1
# @schema
replica: ""
```

#### `exclusiveMinimum`

The value have to be strictly above the given `integer`.

```yaml
# @schema
# exclusiveMinimum: 0
# @schema
replica: ""
```

#### `maximum`

The value have to be below or equal the given `integer`.

```yaml
# @schema
# maximum: 10
# @schema
replica: ""
```

#### `exclusiveMaximum`

The value have to be strictly below the given `integer`.

```yaml
# @schema
# exclusiveMaximum: 5
# @schema
cpu: ""
```

#### `multipleOf`

The value have to be a multiple of the given `integer`.

```yaml
# @schema
# multipleOf: 1024
# @schema
storageCapacity: 2048
```

#### `additionalProperties`

By default, `additionalProperties` is set to `false` unless you use the `-k additionalProperties` option. Useful when you don't know what nested keys you'll have.

```yaml
# @schema
# additionalProperties: true
# @schema
# You'll be able to add as many keys below `env:` as you want without invalidating the schema
env:
  LONG: foo
  LIST: bar
  OF: baz
  VARIABLES: bat

# @schema
# additionalProperties: true
# properties:
#   REQUIRED_VAR:
#     type: string
# @schema
env:
  REQUIRED_VAR: foo
  OPTIONAL_VAR: bar
```

#### `patternProperties`

Mapping schemas to key name patterns. If properties match the patterns, the given schema is applied.

Useful when you work with a long list of keys and want to define a common schema for a group of them, for example.

E.g. `patternProperties."^API_.*"` key defines the pattern whose schema will be applied on any user provided key that match that pattern.

```yaml
# @schema
# type: object
# patternProperties:
#   "^API_.*":
#     type: string
#     pattern: ^api-key
#   "^EMAIL_.*":
#     type: string
#     format: idn-email
# @schema
env:
  API_PROVIDER_ONE: api-key-xxxxx
  API_PROVIDER_TWO: api-key-xxxxx
  EMAIL_ADMIN: admin@example.org
  EMAIL_DEFAULT_USER: user@example.org
```

#### `anyOf`

Allows user to define multiple schema fo a single key. Key can be `anyOf` the given schemas or none of them.

```yaml
# Accepts multiple types
# @schema
# anyOf:
#   - type: string
#   - type: integer
# minimum: 0
# @schema
foot: 1

# The above can be simplified with `type:`
# @schema
# type: [string, integer]
# minimum: 0
# @schema
fool: 1

# A pattern is also possible.
# In this case null or some string starting with foo.
# @schema
# anyOf:
#   - type: "null"
#   - pattern: ^foo
# @schema
bar:
```

#### `oneOf`

Allows user to define multiple schema fo a single key. Key must match `oneOf` the given schemas.

```yaml
# @schema
# oneOf:
#   - type: integer
#   - pattern: Gib$
#   - pattern: gib$
# @schema
storage: 30Gib
```

#### `allOf`

Allows user to define multiple schema for a single key. Key must match `oneOf` the given schemas.

```yaml
# @schema
# allOf:
#   - type: string
#     pattern: Gib$
#   - enum: [5Gib,10Gib,15Gib]
# @schema
storage: 10Gib
```

#### `not`

Allows to define a schema that must not be matched.

```yaml
# @schema
# not:
#   type: string
# @schema
foo: bar
```

#### `if/then/else`

Conditional schema settings with `if`/`then`/`else`

```yaml
# @schema
# anyOf:
#   - type: "null"
#   - type: string
# if:
#   type: "null"
# then:
#   description: It's a null value
# else:
#   description: It's a string
# @schema
unknown: foo
```

#### `minLength`

The value must be an integer greater or equal to zero and defines the minimum length of a string value.

```yaml
# @schema
# minLength: 1
# @schema
namespace: foo
```

#### `maxLength`

The value must be an integer greater than zero and defines the maximum length of a string value.

```yaml
# @schema
# maxLength: 3
# @schema
namespace: foo
```

#### `minItems`

The value must be an integer greater than zero and defines the minimum length of an array value.

```yaml
# @schema
# minItems: 1
# @schema
namespace:
  - foo
```

#### `maxItems`

The value must be an integer greater than zero and defines the maximum length of an array value.

```yaml
# @schema
# maxItems: 2
# @schema
namespace:
  - foo
  - bar
```

#### `uniqueItems`

A schema can ensure that each of the items in an array is unique. Simply set the uniqueItems keyword to true.

```yaml
# @schema
# uniqueItems: true
# @schema
namespace:
  - foo
  - bar
```

#### `$ref`

The value must be an URI or relative file.

Relative files are imported on creation time. If you update the referenced file, you need
to run helm-schema again.

**foo.json:**

```json
{
  "foo": {
    "type": "string",
    "minLength": 10
  }
}
```

```yaml
# @schema
# $ref: foo.json#/foo
# @schema
namespace: foo
```

is the same as

```yaml
# @schema
# type: string
# minLength: 10
# @schema
namespace: foo
```

#### `contains`

Specifies that an array must contain at least one item matching the given schema.

```yaml
# @schema
# type: array
# contains:
#   type: string
#   pattern: ^admin
# @schema
# At least one item must be a string starting with 'admin'
users:
  - admin-user
  - regular-user
```

#### `additionalItems`

Controls validation of array items beyond those specified in an `items` tuple.

```yaml
# @schema
# type: array
# additionalItems: false
# @schema
# No additional items allowed beyond what's defined
fixedArray:
  - foo
  - bar
```

#### `minProperties`

Minimum number of properties an object must have.

```yaml
# @schema
# type: object
# minProperties: 1
# @schema
# Object must have at least one property
config: {}
```

#### `maxProperties`

Maximum number of properties an object can have.

```yaml
# @schema
# type: object
# maxProperties: 5
# @schema
# Object can have at most 5 properties
labels:
  app: myapp
  env: prod
```

#### `propertyNames`

Schema that all property names in an object must match.

```yaml
# @schema
# type: object
# propertyNames:
#   pattern: ^[a-z][a-z0-9-]*$
# @schema
# All property names must be lowercase with hyphens
annotations:
  app-name: myapp
  version: v1
```

#### `dependencies`

Define property dependencies - when one property is present, others must be too.

```yaml
# @schema
# type: object
# dependencies:
#   creditCard: [billingAddress]
#   billingAddress: [creditCard]
# @schema
# If creditCard is present, billingAddress must also be present
payment:
  creditCard: "1234-5678"
  billingAddress: "123 Main St"
```

#### `definitions`

Define reusable schema fragments that can be referenced with `$ref`.

> [!NOTE]
> When referencing external JSON Schema files that use `$defs` (JSON Schema Draft 2019-09+), helm-schema automatically converts them to `definitions` and rewrites `$ref` paths from `#/$defs/` to `#/definitions/` for Draft 7 compatibility.

```yaml
# @schema
# definitions:
#   port:
#     type: integer
#     minimum: 1
#     maximum: 65535
# properties:
#   httpPort:
#     $ref: "#/definitions/port"
#   httpsPort:
#     $ref: "#/definitions/port"
# @schema
service:
  httpPort: 80
  httpsPort: 443
```

#### `$comment`

Add comments for schema maintainers that won't be shown to end users.

```yaml
# @schema
# type: string
# $comment: This field is deprecated and will be removed in v2.0
# @schema
legacyField: foo
```

#### `contentEncoding`

Specify the encoding for string content, such as base64.

```yaml
# @schema
# type: string
# contentEncoding: base64
# @schema
# Value is expected to be base64 encoded
certificate: "LS0tLS1CRUdJTi..."
```

#### `contentMediaType`

Specify the MIME type for string content.

```yaml
# @schema
# type: string
# contentMediaType: application/json
# contentEncoding: base64
# @schema
# Value is base64-encoded JSON
configData: "eyJmb28iOiAiYmFyIn0="
```

## License

[MIT](https://github.com/dadav/helm-schema/blob/main/LICENSE)


================================================
FILE: cliff.toml
================================================
[changelog]
header = ""
body = """
{% if version %}## {{ version }} - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}## Unreleased
{% endif %}

{% for group, commits in commits | group_by(attribute="group") %}
### {{ group }}
{% for commit in commits %}
- {{ commit.message | split(pat="\n") | first | trim }}{% if commit.remote.pr_number %} ([#{{ commit.remote.pr_number }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/pull/{{ commit.remote.pr_number }})){% endif %}{% if commit.remote.username %} by @{{ commit.remote.username }}{% endif %}
{% endfor %}

{% endfor %}
"""
trim = true

[git]
conventional_commits = true
filter_unconventional = true
split_commits = false
protect_breaking_commits = false
filter_commits = true
tag_pattern = "[0-9].*"
sort_commits = "oldest"
commit_parsers = [
  { message = "^feat", group = "Features" },
  { message = "^fix", group = "Bug Fixes" },
  { message = "^docs?", group = "Documentation" },
  { message = "^refactor", group = "Refactoring" },
  { message = "^test", group = "Testing" },
  { message = "^chore", group = "Chores" },
]

[remote.github]
owner = "dadav"
repo = "helm-schema"


================================================
FILE: cmd/helm-schema/cli.go
================================================
package main

import (
	"fmt"
	"os"
	"strings"

	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

func possibleLogLevels() []string {
	levels := make([]string, 0)

	for _, l := range log.AllLevels {
		levels = append(levels, l.String())
	}

	return levels
}

func configureLogging() {
	logLevelName := viper.GetString("log-level")
	logLevel, err := log.ParseLevel(logLevelName)
	if err != nil {
		log.Errorf("Failed to parse provided log level %s: %s", logLevelName, err)
		os.Exit(1)
	}

	log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
	log.SetLevel(logLevel)
}

func newCommand(run func(cmd *cobra.Command, args []string) error) (*cobra.Command, error) {
	cmd := &cobra.Command{
		Use:           "helm-schema",
		Short:         "helm-schema automatically generates a jsonschema file for helm charts from values files",
		Version:       version,
		RunE:          run,
		SilenceUsage:  true,
		SilenceErrors: true,
	}

	logLevelUsage := fmt.Sprintf(
		"level of logs that should printed, one of (%s)",
		strings.Join(possibleLogLevels(), ", "),
	)
	cmd.PersistentFlags().
		StringP("chart-search-root", "c", ".", "directory to search recursively within for charts")
	cmd.PersistentFlags().
		BoolP("dry-run", "d", false, "don't actually create files just print to stdout passed")
	cmd.PersistentFlags().
		BoolP("append-newline", "a", false, "append newline to generated jsonschema at the end of the file")
	cmd.PersistentFlags().
		BoolP("keep-full-comment", "s", false, "keep the whole leading comment (default: cut at empty line)")
	cmd.PersistentFlags().
		BoolP("uncomment", "u", false, "consider yaml which is commented out")
	cmd.PersistentFlags().
		BoolP("helm-docs-compatibility-mode", "p", false, "parse and use helm-docs comments")
	cmd.PersistentFlags().
		BoolP("dont-strip-helm-docs-prefix", "x", false, "disable the removal of the helm-docs prefix (--)")
	cmd.PersistentFlags().
		BoolP("no-dependencies", "n", false, "skip dependency charts: don't merge them into parents and don't generate their schemas")
	cmd.PersistentFlags().
		BoolP("add-schema-reference", "r", false, "add reference to schema in values.yaml if not found")
	cmd.PersistentFlags().StringP("log-level", "l", "info", logLevelUsage)
	cmd.PersistentFlags().
		StringSliceP("value-files", "f", []string{"values.yaml"}, "filenames to check for chart values")
	cmd.PersistentFlags().
		StringP("output-file", "o", "values.schema.json", "jsonschema file path relative to each chart directory to which jsonschema will be written")
	cmd.PersistentFlags().
		StringSliceP("skip-auto-generation", "k", []string{}, "comma separated list of fields to skip from being created by default (possible: title, description, required, default, additionalProperties)")
	cmd.PersistentFlags().
		StringSliceP("dependencies-filter", "i", []string{}, "only generate schema for specified dependencies (comma-separated list of dependency names)")
	cmd.PersistentFlags().
		BoolP("dont-add-global", "g", false, "dont auto add global property")
	cmd.PersistentFlags().
		BoolP("skip-dependencies-schema-validation", "m", false, "skip schema validation for dependencies by setting additionalProperties to true and removing from required")
	cmd.PersistentFlags().
		BoolP("allow-circular-dependencies", "w", false, "allow circular dependencies between charts (will log a warning instead of failing)")
	cmd.PersistentFlags().
		BoolP("annotate", "A", false, "write inferred @schema annotations into values.yaml files for unannotated keys")
	cmd.PersistentFlags().
		BoolP("keep-existing-dep-schemas", "K", false, "use dependency charts' pre-existing values.schema.json instead of regenerating from values.yaml")

	viper.AutomaticEnv()
	viper.SetEnvPrefix("HELM_SCHEMA")
	viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
	err := viper.BindPFlags(cmd.PersistentFlags())

	return cmd, err
}


================================================
FILE: cmd/helm-schema/main.go
================================================
package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"slices"
	"strings"
	"sync"

	"github.com/dadav/helm-schema/pkg/chart"
	"github.com/dadav/helm-schema/pkg/chart/searching"
	"github.com/dadav/helm-schema/pkg/schema"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

// getDependencyNames extracts dependency names (or aliases if present) from a chart
// filtering based on the provided dependenciesFilterMap
func getDependencyNames(dependencies []*chart.Dependency, dependenciesFilterMap map[string]bool) []string {
	var depNames []string
	for _, dep := range dependencies {
		if len(dependenciesFilterMap) > 0 && !dependenciesFilterMap[dep.Name] {
			continue
		}
		if dep.Alias != "" {
			depNames = append(depNames, dep.Alias)
		} else if dep.Name != "" {
			depNames = append(depNames, dep.Name)
		}
	}
	return depNames
}

// mergeSchemaProperties merges properties from source to target schema.
// It skips "global" and properties in the skip map, and returns merged property names.
// Follows Helm's value coalescing behavior with one exception:
// - If target has explicit @schema annotation (HasData=true), target wins
// - If target only has inferred schema (HasData=false), source wins
func mergeSchemaProperties(
	target *schema.Schema,
	source *schema.Schema,
	skip map[string]bool,
	sourceName string,
	targetName string,
) map[string]bool {
	merged := make(map[string]bool)

	if source.Properties == nil {
		return merged
	}

	if target.Properties == nil {
		target.Properties = make(map[string]*schema.Schema)
	}

	for propName, propSchema := range source.Properties {
		if propName == "global" {
			continue
		}
		if skip != nil && skip[propName] {
			continue
		}
		existingProp, exists := target.Properties[propName]
		if !exists {
			target.Properties[propName] = propSchema
			merged[propName] = true
		} else if !existingProp.HasData && propSchema.HasData {
			// Target only has inferred schema, source has explicit annotation - source wins
			target.Properties[propName] = propSchema
			merged[propName] = true
			log.Debugf("Property %s from %s replaces inferred schema in %s", propName, sourceName, targetName)
		} else if existingProp.HasData {
			// Target has explicit @schema annotation, keep it
			log.Debugf("Property %s from %s skipped: %s has explicit @schema annotation", propName, sourceName, targetName)
		} else {
			// Both are inferred schemas, keep target (first wins)
			log.Debugf("Property %s from %s skipped: both schemas are inferred, keeping first", propName, sourceName)
		}
	}

	return merged
}

// processImportValues processes the import-values directive for a dependency.
// It returns a map of property names that were imported (to track what was handled).
func processImportValues(
	parentSchema *schema.Schema,
	depSchema *schema.Schema,
	dep *chart.Dependency,
	parentChartName string,
) map[string]bool {
	importedProps := make(map[string]bool)

	if len(dep.ImportValues) == 0 {
		return importedProps
	}

	for _, importValue := range dep.ImportValues {
		var childPath, parentPath string

		switch v := importValue.(type) {
		case string:
			// Simple form: "defaults" -> imports from exports.<value> to root
			childPath = "exports." + v
			parentPath = ""
		case map[string]interface{}:
			// Complex form: {child: "path", parent: "path"}
			if child, ok := v["child"].(string); ok {
				childPath = child
			}
			if parent, ok := v["parent"].(string); ok {
				parentPath = parent
			}
		case map[interface{}]interface{}:
			// YAML sometimes produces this type variation
			if child, ok := v["child"].(string); ok {
				childPath = child
			}
			if parent, ok := v["parent"].(string); ok {
				parentPath = parent
			}
		default:
			log.Warnf("Unknown import-values format for dependency %s in chart %s: %T", dep.Name, parentChartName, importValue)
			continue
		}

		if childPath == "" {
			log.Warnf("Empty child path in import-values for dependency %s in chart %s", dep.Name, parentChartName)
			continue
		}

		// Get the source schema from the dependency
		sourceSchema := depSchema.GetPropertyAtPath(childPath)
		if sourceSchema == nil {
			log.Warnf("Could not find path %q in dependency %s schema for chart %s", childPath, dep.Name, parentChartName)
			continue
		}

		if sourceSchema.Properties == nil {
			log.Warnf("No properties found at path %q in dependency %s for chart %s", childPath, dep.Name, parentChartName)
			continue
		}

		// Determine target schema in parent
		var targetSchema *schema.Schema
		if parentPath == "" {
			targetSchema = parentSchema
		} else {
			targetSchema = parentSchema.SetPropertyAtPath(parentPath)
		}

		merged := mergeSchemaProperties(
			targetSchema,
			sourceSchema,
			nil,
			fmt.Sprintf("import-values of %s", dep.Name),
			parentChartName,
		)
		targetPathDisplay := parentPath
		if targetPathDisplay == "" {
			targetPathDisplay = "root"
		}
		for k := range merged {
			importedProps[k] = true
			log.Debugf("Imported property %q from %s.%s to %s in chart %s",
				k, dep.Name, childPath, targetPathDisplay, parentChartName)
		}
	}

	return importedProps
}

func exec(cmd *cobra.Command, _ []string) error {
	configureLogging()

	var skipAutoGeneration, valueFileNames []string

	chartSearchRoot := viper.GetString("chart-search-root")
	dryRun := viper.GetBool("dry-run")
	noDeps := viper.GetBool("no-dependencies")
	addSchemaReference := viper.GetBool("add-schema-reference")
	keepFullComment := viper.GetBool("keep-full-comment")
	helmDocsCompatibilityMode := viper.GetBool("helm-docs-compatibility-mode")
	uncomment := viper.GetBool("uncomment")
	outFile := viper.GetString("output-file")
	dontRemoveHelmDocsPrefix := viper.GetBool("dont-strip-helm-docs-prefix")
	appendNewline := viper.GetBool("append-newline")
	dependenciesFilter := viper.GetStringSlice("dependencies-filter")
	dependenciesFilterMap := make(map[string]bool)
	dontAddGlobal := viper.GetBool("dont-add-global")
	skipDepsSchemaValidation := viper.GetBool("skip-dependencies-schema-validation")
	allowCircularDeps := viper.GetBool("allow-circular-dependencies")
	annotate := viper.GetBool("annotate")
	keepExistingDepSchemas := viper.GetBool("keep-existing-dep-schemas")
	for _, dep := range dependenciesFilter {
		dependenciesFilterMap[dep] = true
	}
	if err := viper.UnmarshalKey("value-files", &valueFileNames); err != nil {
		return err
	}
	if err := viper.UnmarshalKey("skip-auto-generation", &skipAutoGeneration); err != nil {
		return err
	}
	workersCount := runtime.NumCPU() * 2

	skipConfig, err := schema.NewSkipAutoGenerationConfig(skipAutoGeneration)
	if err != nil {
		return err
	}

	queue := make(chan string)
	resultsChan := make(chan schema.Result)
	results := []*schema.Result{}
	errs := make(chan error, 100) // Buffered to prevent deadlock when errors occur before goroutines start
	done := make(chan struct{})

	tempDir := searching.SearchArchivesOpenTemp(chartSearchRoot, errs)
	if tempDir != "" {
		defer os.RemoveAll(tempDir)
	}

	go searching.SearchFiles(chartSearchRoot, chartSearchRoot, "Chart.yaml", dependenciesFilterMap, queue, errs)

	wg := sync.WaitGroup{}

	for i := 0; i < workersCount; i++ {
		wg.Add(1)

		go func() {
			defer wg.Done()
			schema.Worker(
				dryRun,
				uncomment,
				addSchemaReference,
				keepFullComment,
				helmDocsCompatibilityMode,
				dontRemoveHelmDocsPrefix,
				dontAddGlobal,
				annotate,
				valueFileNames,
				skipConfig,
				outFile,
				queue,
				resultsChan,
			)
		}()
	}

	// Close resultsChan after all workers are done
	go func() {
		wg.Wait()
		close(resultsChan)
		close(done)
	}()

	// Collect results and errors until both channels are closed
	resultsChanOpen := true
	for resultsChanOpen {
		select {
		case err, ok := <-errs:
			if ok {
				log.Error(err)
			}
		case res, ok := <-resultsChan:
			if !ok {
				resultsChanOpen = false
			} else {
				results = append(results, &res)
			}
		}
	}

	// Drain any remaining errors
drainErrors:
	for {
		select {
		case err, ok := <-errs:
			if ok {
				log.Error(err)
			}
		default:
			break drainErrors
		}
	}

	// In annotate mode, just report errors and return (no schema generation)
	if annotate {
		foundErrors := false
		for _, result := range results {
			if len(result.Errors) > 0 {
				foundErrors = true
				if result.Chart != nil {
					log.Errorf("Found %d errors while annotating chart %s (%s)", len(result.Errors), result.Chart.Name, result.ChartPath)
				} else {
					log.Errorf("Found %d errors while annotating chart %s", len(result.Errors), result.ChartPath)
				}
				for _, err := range result.Errors {
					log.Error(err)
				}
			}
		}
		if foundErrors {
			return errors.New("some errors were found")
		}
		return nil
	}

	if !noDeps {
		results, err = schema.TopoSort(results, allowCircularDeps)
		if err != nil {
			if _, ok := err.(*schema.CircularError); ok {
				log.Errorf("Error while sorting results: %s", err)
				return err
			} else {
				log.Warnf("Could not sort results: %s", err)
			}
		}
	}

	// Identify charts that are declared as dependencies of some other discovered
	// chart. Used both to skip dependency charts entirely with --no-dependencies
	// and to opt-in reuse of a dependency's pre-existing schema.
	isDependencyChart := make(map[string]bool)
	for _, result := range results {
		if result.Chart == nil || len(result.Errors) > 0 {
			continue
		}
		for _, dep := range result.Chart.Dependencies {
			isDependencyChart[dep.Name] = true
		}
	}

	// For dependency charts with pre-existing schema files, load them instead of
	// using the worker-generated schema from values.yaml. Opt-in via
	// --keep-existing-dep-schemas; default is to regenerate every discovered
	// chart's schema.
	if !noDeps && keepExistingDepSchemas {
		for _, result := range results {
			if result.Chart == nil || len(result.Errors) > 0 {
				continue
			}
			if !isDependencyChart[result.Chart.Name] {
				continue
			}
			schemaPath := filepath.Join(filepath.Dir(result.ChartPath), outFile)
			schemaData, err := os.ReadFile(schemaPath)
			if err != nil {
				continue
			}
			var existingSchema schema.Schema
			if err := json.Unmarshal(schemaData, &existingSchema); err != nil {
				log.Warnf("Found existing %s for dependency %s but failed to parse it: %s", outFile, result.Chart.Name, err)
				continue
			}
			log.Debugf("Using pre-existing schema for dependency chart %s", result.Chart.Name)
			result.Schema = existingSchema
			result.PreExistingSchema = true
		}
	}

	conditionsToPatch := make(map[string][][]string)
	if !noDeps {
		for _, result := range results {
			if len(result.Errors) > 0 {
				continue
			}
			for _, dep := range result.Chart.Dependencies {
				if len(dependenciesFilterMap) > 0 && !dependenciesFilterMap[dep.Name] {
					continue
				}

				if dep.Condition != "" {
					conditionKeys := strings.Split(dep.Condition, ".")
					if len(conditionKeys) == 1 {
						continue
					}
					targetName := conditionKeys[0]
					if dep.Alias != "" && dep.Alias == conditionKeys[0] {
						targetName = dep.Name
					}
					if targetName != "" {
						conditionsToPatch[targetName] = append(conditionsToPatch[targetName], conditionKeys[1:])
					}
				}
			}
		}
	}

	chartNameToResult := make(map[string]*schema.Result)
	foundErrors := false

	for _, result := range results {
		if len(result.Errors) > 0 {
			foundErrors = true
			if result.Chart != nil {
				log.Errorf(
					"Found %d errors while processing the chart %s (%s)",
					len(result.Errors),
					result.Chart.Name,
					result.ChartPath,
				)
			} else {
				log.Errorf("Found %d errors while processing the chart %s", len(result.Errors), result.ChartPath)
			}
			for _, err := range result.Errors {
				log.Error(err)
			}
			continue
		}

		if result.Chart == nil {
			log.Warnf("Skipping result with nil Chart at path: %s", result.ChartPath)
			continue
		}

		// With --no-dependencies, skip charts that are declared as dependencies of
		// some other discovered chart. Top-level charts are still processed.
		if noDeps && isDependencyChart[result.Chart.Name] {
			log.Debugf("Skipping dependency chart %s (--no-dependencies)", result.Chart.Name)
			continue
		}

		log.Debugf("Processing result for chart: %s (%s)", result.Chart.Name, result.ChartPath)
		if !noDeps {
			chartNameToResult[result.Chart.Name] = result
			log.Debugf("Stored chart %s in chartNameToResult", result.Chart.Name)

			if patches, ok := conditionsToPatch[result.Chart.Name]; ok {
				for _, patch := range patches {
					schemaToPatch := &result.Schema
					lastIndex := len(patch) - 1
					for i, key := range patch {
						// Ensure Properties map is initialized
						if schemaToPatch.Properties == nil {
							schemaToPatch.Properties = make(map[string]*schema.Schema)
						}
						if alreadyPresentSchema, ok := schemaToPatch.Properties[key]; !ok {
							log.Debugf(
								"Patching conditional field \"%s\" into schema of chart %s",
								key,
								result.Chart.Name,
							)
							if i == lastIndex {
								schemaToPatch.Properties[key] = &schema.Schema{
									Type:        []string{"boolean"},
									Title:       key,
									Description: "Conditional property used in parent chart",
								}
							} else {
								schemaToPatch.Properties[key] = &schema.Schema{
									Type:       []string{"object"},
									Title:      key,
									Properties: make(map[string]*schema.Schema),
								}
								schemaToPatch = schemaToPatch.Properties[key]
							}
						} else {
							schemaToPatch = alreadyPresentSchema
						}
					}
				}
			}

			for _, dep := range result.Chart.Dependencies {
				if len(dependenciesFilterMap) > 0 && !dependenciesFilterMap[dep.Name] {
					continue
				}

				if dep.Name != "" {
					if dependencyResult, ok := chartNameToResult[dep.Name]; ok {
						log.Debugf(
							"Found chart of dependency %s (%s)",
							dependencyResult.Chart.Name,
							dependencyResult.ChartPath,
						)

						// Process import-values first (before regular dependency nesting)
						importedProps := processImportValues(
							&result.Schema,
							&dependencyResult.Schema,
							dep,
							result.Chart.Name,
						)
						hasImportValues := len(dep.ImportValues) > 0

						// Check if this is a library chart
						if dependencyResult.Chart.Type == "library" {
							// For library charts, merge properties directly into parent schema
							log.Debugf("Merging library chart %s properties into parent chart %s at top level", dep.Name, result.Chart.Name)
							mergeSchemaProperties(
								&result.Schema,
								&dependencyResult.Schema,
								importedProps,
								fmt.Sprintf("library chart %s", dep.Name),
								fmt.Sprintf("parent chart %s", result.Chart.Name),
							)
						} else if !hasImportValues {
							// For non-library charts WITHOUT import-values, nest under dependency name
							// (If import-values is used, user explicitly controls what's imported)
							depSchema := schema.Schema{
								Type:        []string{"object"},
								Title:       dep.Name,
								Description: dependencyResult.Chart.Description,
								Properties:  dependencyResult.Schema.Properties,
							}
							if dep.Condition != "" && !strings.Contains(dep.Condition, ".") {
								depSchema.Type = []string{"object", "boolean"}
							}
							depSchema.DisableRequiredProperties()

							if dep.Alias != "" {
								result.Schema.Properties[dep.Alias] = &depSchema
							} else {
								result.Schema.Properties[dep.Name] = &depSchema
							}
						}

					} else {
						log.Warnf("Dependency (%s->%s) specified but no schema found. If you want to create jsonschemas for external dependencies, you need to run helm dep up", result.Chart.Name, dep.Name)
					}
				} else {
					log.Warnf("Dependency without name found (checkout %s).", result.ChartPath)
				}
			}
		}

		// Handle skip-dependencies-schema-validation flag
		if skipDepsSchemaValidation && !noDeps {
			// Collect dependency names using helper function
			depNames := getDependencyNames(result.Chart.Dependencies, dependenciesFilterMap)

			// Remove dependency names from required properties
			oldRequired := result.Schema.Required.Strings
			var newRequired []string
			for _, n := range oldRequired {
				if !slices.Contains(depNames, n) {
					newRequired = append(newRequired, n)
				}
			}
			result.Schema.Required.Strings = newRequired

			// Set additionalProperties to true for dependency schemas
			for _, depName := range depNames {
				if prop, ok := result.Schema.Properties[depName]; ok && prop != nil {
					log.Debugf("Setting additionalProperties to true for dependency %s in chart %s", depName, result.Chart.Name)
					additionalPropsTrue := true
					prop.AdditionalProperties = &additionalPropsTrue
				}
			}
		}

		// Hoist all nested definitions to the root level so $ref pointers resolve correctly
		result.Schema.HoistDefinitions()

		// Skip writing output for dependency charts with pre-existing schema files
		if result.PreExistingSchema {
			log.Debugf("Skipping output for dependency chart %s: using pre-existing schema", result.Chart.Name)
			continue
		}

		jsonStr, err := result.Schema.ToJson()
		if err != nil {
			log.Error(err)
			continue
		}

		if appendNewline {
			jsonStr = append(jsonStr, '\n')
		}

		if dryRun {
			log.Infof("Printing jsonschema for %s chart (%s)", result.Chart.Name, result.ChartPath)
			if appendNewline {
				fmt.Printf("%s", jsonStr)
			} else {
				fmt.Printf("%s\n", jsonStr)
			}
		} else {
			chartBasePath := filepath.Dir(result.ChartPath)
			if err := os.WriteFile(filepath.Join(chartBasePath, outFile), jsonStr, 0o644); err != nil {
				errs <- err
				continue
			}
		}
	}
	if foundErrors {
		return errors.New("some errors were found")
	}
	return nil
}

func main() {
	command, err := newCommand(exec)
	if err != nil {
		log.Errorf("Failed to create the CLI commander: %s", err)
		os.Exit(1)
	}

	if err := command.Execute(); err != nil {
		log.Errorf("Execution error: %s", err)
		os.Exit(1)
	}
}


================================================
FILE: cmd/helm-schema/main_test.go
================================================
package main

import (
	"encoding/json"
	"os"
	"path/filepath"
	"testing"

	"github.com/spf13/viper"
	"github.com/stretchr/testify/assert"
)

type stringOrArray []string

func (s *stringOrArray) UnmarshalJSON(value []byte) error {
	if len(value) == 0 {
		return nil
	}
	if value[0] == '"' {
		var single string
		if err := json.Unmarshal(value, &single); err != nil {
			return err
		}
		*s = []string{single}
		return nil
	}
	var multi []string
	if err := json.Unmarshal(value, &multi); err != nil {
		return err
	}
	*s = multi
	return nil
}

type schemaDoc struct {
	Properties map[string]schemaProperty `json:"properties"`
}

type schemaProperty struct {
	Type       stringOrArray              `json:"type"`
	Properties map[string]schemaProperty  `json:"properties"`
}

func TestExec_ConditionPatchingAndRootConditions(t *testing.T) {
	tmpDir := t.TempDir()

	writeFile := func(relPath, content string) {
		path := filepath.Join(tmpDir, relPath)
		err := os.MkdirAll(filepath.Dir(path), 0o755)
		assert.NoError(t, err)
		err = os.WriteFile(path, []byte(content), 0o644)
		assert.NoError(t, err)
	}

	writeFile("dep/Chart.yaml", `
apiVersion: v2
name: dep
version: 1.0.0
`)
	writeFile("dep/values.yaml", `
key: value
`)

	writeFile("dep2/Chart.yaml", `
apiVersion: v2
name: dep2
version: 1.0.0
`)
	writeFile("dep2/values.yaml", `
key: value
`)

	writeFile("parent1/Chart.yaml", `
apiVersion: v2
name: parent1
version: 1.0.0
dependencies:
  - name: dep
    version: 1.0.0
    condition: dep.enabled
  - name: dep2
    version: 1.0.0
    condition: dep2
`)
	writeFile("parent1/values.yaml", `
root: value
`)

	writeFile("parent2/Chart.yaml", `
apiVersion: v2
name: parent2
version: 1.0.0
dependencies:
  - name: dep
    version: 1.0.0
    condition: dep.extra.flag
`)
	writeFile("parent2/values.yaml", `
root: value
`)

	viper.Reset()
	viper.Set("chart-search-root", tmpDir)
	viper.Set("dry-run", false)
	viper.Set("no-dependencies", false)
	viper.Set("add-schema-reference", false)
	viper.Set("keep-full-comment", false)
	viper.Set("helm-docs-compatibility-mode", false)
	viper.Set("uncomment", false)
	viper.Set("output-file", "values.schema.json")
	viper.Set("dont-strip-helm-docs-prefix", false)
	viper.Set("append-newline", false)
	viper.Set("dependencies-filter", []string{})
	viper.Set("dont-add-global", true)
	viper.Set("skip-dependencies-schema-validation", false)
	viper.Set("allow-circular-dependencies", false)
	viper.Set("annotate", false)
	viper.Set("keep-existing-dep-schemas", false)
	viper.Set("value-files", []string{"values.yaml"})
	viper.Set("skip-auto-generation", []string{})
	viper.Set("log-level", "info")

	err := exec(nil, nil)
	assert.NoError(t, err)

	depSchemaPath := filepath.Join(tmpDir, "dep", "values.schema.json")
	depSchemaBytes, err := os.ReadFile(depSchemaPath)
	assert.NoError(t, err)

	var depSchema schemaDoc
	err = json.Unmarshal(depSchemaBytes, &depSchema)
	assert.NoError(t, err)

	enabledProp, ok := depSchema.Properties["enabled"]
	assert.True(t, ok)
	assert.Contains(t, []string(enabledProp.Type), "boolean")

	extraProp, ok := depSchema.Properties["extra"]
	assert.True(t, ok)
	flagProp, ok := extraProp.Properties["flag"]
	assert.True(t, ok)
	assert.Contains(t, []string(flagProp.Type), "boolean")

	parent1SchemaPath := filepath.Join(tmpDir, "parent1", "values.schema.json")
	parent1SchemaBytes, err := os.ReadFile(parent1SchemaPath)
	assert.NoError(t, err)

	var parent1Schema schemaDoc
	err = json.Unmarshal(parent1SchemaBytes, &parent1Schema)
	assert.NoError(t, err)

	dep2Prop, ok := parent1Schema.Properties["dep2"]
	assert.True(t, ok)
	assert.Contains(t, []string(dep2Prop.Type), "object")
	assert.Contains(t, []string(dep2Prop.Type), "boolean")
}

func TestExec_DependencyFilterSkipsConditionPatching(t *testing.T) {
	tmpDir := t.TempDir()

	writeFile := func(relPath, content string) {
		path := filepath.Join(tmpDir, relPath)
		err := os.MkdirAll(filepath.Dir(path), 0o755)
		assert.NoError(t, err)
		err = os.WriteFile(path, []byte(content), 0o644)
		assert.NoError(t, err)
	}

	writeFile("dep/Chart.yaml", `
apiVersion: v2
name: dep
version: 1.0.0
`)
	writeFile("dep/values.yaml", `
key: value
`)

	writeFile("dep2/Chart.yaml", `
apiVersion: v2
name: dep2
version: 1.0.0
`)
	writeFile("dep2/values.yaml", `
key: value
`)

	writeFile("Chart.yaml", `
apiVersion: v2
name: parent
version: 1.0.0
dependencies:
  - name: dep
    version: 1.0.0
    condition: dep.enabled
  - name: dep2
    version: 1.0.0
    condition: dep2.flag
`)
	writeFile("values.yaml", `
root: value
`)

	viper.Reset()
	viper.Set("chart-search-root", tmpDir)
	viper.Set("dry-run", false)
	viper.Set("no-dependencies", false)
	viper.Set("add-schema-reference", false)
	viper.Set("keep-full-comment", false)
	viper.Set("helm-docs-compatibility-mode", false)
	viper.Set("uncomment", false)
	viper.Set("output-file", "values.schema.json")
	viper.Set("dont-strip-helm-docs-prefix", false)
	viper.Set("append-newline", false)
	viper.Set("dependencies-filter", []string{"dep"})
	viper.Set("dont-add-global", true)
	viper.Set("skip-dependencies-schema-validation", false)
	viper.Set("allow-circular-dependencies", false)
	viper.Set("annotate", false)
	viper.Set("keep-existing-dep-schemas", false)
	viper.Set("value-files", []string{"values.yaml"})
	viper.Set("skip-auto-generation", []string{})
	viper.Set("log-level", "info")

	err := exec(nil, nil)
	assert.NoError(t, err)

	parentSchemaPath := filepath.Join(tmpDir, "values.schema.json")
	parentSchemaBytes, err := os.ReadFile(parentSchemaPath)
	assert.NoError(t, err)

	var parentSchema schemaDoc
	err = json.Unmarshal(parentSchemaBytes, &parentSchema)
	assert.NoError(t, err)

	_, hasDep := parentSchema.Properties["dep"]
	_, hasDep2 := parentSchema.Properties["dep2"]
	assert.True(t, hasDep)
	assert.False(t, hasDep2)

	depSchemaPath := filepath.Join(tmpDir, "dep", "values.schema.json")
	depSchemaBytes, err := os.ReadFile(depSchemaPath)
	assert.NoError(t, err)

	var depSchema schemaDoc
	err = json.Unmarshal(depSchemaBytes, &depSchema)
	assert.NoError(t, err)

	enabledProp, ok := depSchema.Properties["enabled"]
	assert.True(t, ok)
	assert.Contains(t, []string(enabledProp.Type), "boolean")

	dep2SchemaPath := filepath.Join(tmpDir, "dep2", "values.schema.json")
	_, err = os.Stat(dep2SchemaPath)
	assert.True(t, os.IsNotExist(err))
}

func TestExec_DependencyAliasConditionPatching(t *testing.T) {
	tmpDir := t.TempDir()

	writeFile := func(relPath, content string) {
		path := filepath.Join(tmpDir, relPath)
		err := os.MkdirAll(filepath.Dir(path), 0o755)
		assert.NoError(t, err)
		err = os.WriteFile(path, []byte(content), 0o644)
		assert.NoError(t, err)
	}

	writeFile("dep/Chart.yaml", `
apiVersion: v2
name: dep
version: 1.0.0
`)
	writeFile("dep/values.yaml", `
key: value
`)

	writeFile("Chart.yaml", `
apiVersion: v2
name: parent
version: 1.0.0
dependencies:
  - name: dep
    alias: depalias
    version: 1.0.0
    condition: depalias.enabled
`)
	writeFile("values.yaml", `
root: value
`)

	viper.Reset()
	viper.Set("chart-search-root", tmpDir)
	viper.Set("dry-run", false)
	viper.Set("no-dependencies", false)
	viper.Set("add-schema-reference", false)
	viper.Set("keep-full-comment", false)
	viper.Set("helm-docs-compatibility-mode", false)
	viper.Set("uncomment", false)
	viper.Set("output-file", "values.schema.json")
	viper.Set("dont-strip-helm-docs-prefix", false)
	viper.Set("append-newline", false)
	viper.Set("dependencies-filter", []string{})
	viper.Set("dont-add-global", true)
	viper.Set("skip-dependencies-schema-validation", false)
	viper.Set("allow-circular-dependencies", false)
	viper.Set("annotate", false)
	viper.Set("keep-existing-dep-schemas", false)
	viper.Set("value-files", []string{"values.yaml"})
	viper.Set("skip-auto-generation", []string{})
	viper.Set("log-level", "info")

	err := exec(nil, nil)
	assert.NoError(t, err)

	depSchemaPath := filepath.Join(tmpDir, "dep", "values.schema.json")
	depSchemaBytes, err := os.ReadFile(depSchemaPath)
	assert.NoError(t, err)

	var depSchema schemaDoc
	err = json.Unmarshal(depSchemaBytes, &depSchema)
	assert.NoError(t, err)

	enabledProp, ok := depSchema.Properties["enabled"]
	assert.True(t, ok)
	assert.Contains(t, []string(enabledProp.Type), "boolean")

	parentSchemaPath := filepath.Join(tmpDir, "values.schema.json")
	parentSchemaBytes, err := os.ReadFile(parentSchemaPath)
	assert.NoError(t, err)

	var parentSchema schemaDoc
	err = json.Unmarshal(parentSchemaBytes, &parentSchema)
	assert.NoError(t, err)

	_, ok = parentSchema.Properties["depalias"]
	assert.True(t, ok)
}

func TestExec_KeepExistingDepSchemasPreservesDependencySchema(t *testing.T) {
	tmpDir := t.TempDir()

	writeFile := func(relPath, content string) {
		path := filepath.Join(tmpDir, relPath)
		err := os.MkdirAll(filepath.Dir(path), 0o755)
		assert.NoError(t, err)
		err = os.WriteFile(path, []byte(content), 0o644)
		assert.NoError(t, err)
	}

	writeFile("dep/Chart.yaml", `
apiVersion: v2
name: dep
version: 1.0.0
`)
	writeFile("dep/values.yaml", `
port: 8080
`)
	preExistingDepSchema := `{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "port": {
      "type": "integer",
      "description": "The port to listen on",
      "minimum": 1,
      "maximum": 65535,
      "x-custom-annotation": "preserve-me"
    }
  },
  "required": ["port"]
}
`
	writeFile("dep/values.schema.json", preExistingDepSchema)

	writeFile("parent/Chart.yaml", `
apiVersion: v2
name: parent
version: 1.0.0
dependencies:
  - name: dep
    version: 1.0.0
    repository: file://../dep
`)
	writeFile("parent/values.yaml", `
root: value
`)

	viper.Reset()
	viper.Set("chart-search-root", tmpDir)
	viper.Set("dry-run", false)
	viper.Set("no-dependencies", false)
	viper.Set("add-schema-reference", false)
	viper.Set("keep-full-comment", false)
	viper.Set("helm-docs-compatibility-mode", false)
	viper.Set("uncomment", false)
	viper.Set("output-file", "values.schema.json")
	viper.Set("dont-strip-helm-docs-prefix", false)
	viper.Set("append-newline", false)
	viper.Set("dependencies-filter", []string{})
	viper.Set("dont-add-global", true)
	viper.Set("skip-dependencies-schema-validation", false)
	viper.Set("allow-circular-dependencies", false)
	viper.Set("annotate", false)
	viper.Set("keep-existing-dep-schemas", true)
	viper.Set("value-files", []string{"values.yaml"})
	viper.Set("skip-auto-generation", []string{})
	viper.Set("log-level", "info")

	err := exec(nil, nil)
	assert.NoError(t, err)

	depSchemaPath := filepath.Join(tmpDir, "dep", "values.schema.json")
	depSchemaBytes, err := os.ReadFile(depSchemaPath)
	assert.NoError(t, err)
	assert.Equal(t, preExistingDepSchema, string(depSchemaBytes),
		"dependency's pre-existing values.schema.json must not be overwritten when --keep-existing-dep-schemas is set")

	parentSchemaPath := filepath.Join(tmpDir, "parent", "values.schema.json")
	parentSchemaBytes, err := os.ReadFile(parentSchemaPath)
	assert.NoError(t, err)

	var parentRaw map[string]any
	err = json.Unmarshal(parentSchemaBytes, &parentRaw)
	assert.NoError(t, err)

	props, _ := parentRaw["properties"].(map[string]any)
	depNode, ok := props["dep"].(map[string]any)
	assert.True(t, ok, "parent must nest dependency schema under dep key")
	depProps, _ := depNode["properties"].(map[string]any)
	portNode, ok := depProps["port"].(map[string]any)
	assert.True(t, ok, "parent must include port from merged dependency schema")
	assert.Equal(t, "preserve-me", portNode["x-custom-annotation"],
		"pre-existing x-* annotations must be carried into the merged parent schema")
}

func TestExec_DefaultRegeneratesDependencySchema(t *testing.T) {
	tmpDir := t.TempDir()

	writeFile := func(relPath, content string) {
		path := filepath.Join(tmpDir, relPath)
		err := os.MkdirAll(filepath.Dir(path), 0o755)
		assert.NoError(t, err)
		err = os.WriteFile(path, []byte(content), 0o644)
		assert.NoError(t, err)
	}

	writeFile("dep/Chart.yaml", `
apiVersion: v2
name: dep
version: 1.0.0
`)
	writeFile("dep/values.yaml", `
port: 8080
`)
	preExistingDepSchema := `{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "port": {
      "type": "integer",
      "x-custom-annotation": "should-be-lost"
    }
  }
}
`
	writeFile("dep/values.schema.json", preExistingDepSchema)

	writeFile("parent/Chart.yaml", `
apiVersion: v2
name: parent
version: 1.0.0
dependencies:
  - name: dep
    version: 1.0.0
    repository: file://../dep
`)
	writeFile("parent/values.yaml", `
root: value
`)

	viper.Reset()
	viper.Set("chart-search-root", tmpDir)
	viper.Set("dry-run", false)
	viper.Set("no-dependencies", false)
	viper.Set("add-schema-reference", false)
	viper.Set("keep-full-comment", false)
	viper.Set("helm-docs-compatibility-mode", false)
	viper.Set("uncomment", false)
	viper.Set("output-file", "values.schema.json")
	viper.Set("dont-strip-helm-docs-prefix", false)
	viper.Set("append-newline", false)
	viper.Set("dependencies-filter", []string{})
	viper.Set("dont-add-global", true)
	viper.Set("skip-dependencies-schema-validation", false)
	viper.Set("allow-circular-dependencies", false)
	viper.Set("annotate", false)
	viper.Set("keep-existing-dep-schemas", false)
	viper.Set("value-files", []string{"values.yaml"})
	viper.Set("skip-auto-generation", []string{})
	viper.Set("log-level", "info")

	err := exec(nil, nil)
	assert.NoError(t, err)

	depSchemaPath := filepath.Join(tmpDir, "dep", "values.schema.json")
	depSchemaBytes, err := os.ReadFile(depSchemaPath)
	assert.NoError(t, err)
	assert.NotEqual(t, preExistingDepSchema, string(depSchemaBytes),
		"default mode must regenerate a dependency's values.schema.json")
}

func TestExec_NoDependenciesSkipsDependencyCharts(t *testing.T) {
	tmpDir := t.TempDir()

	writeFile := func(relPath, content string) {
		path := filepath.Join(tmpDir, relPath)
		err := os.MkdirAll(filepath.Dir(path), 0o755)
		assert.NoError(t, err)
		err = os.WriteFile(path, []byte(content), 0o644)
		assert.NoError(t, err)
	}

	writeFile("foo/Chart.yaml", `
apiVersion: v2
name: foo
version: 1.0.0
dependencies:
  - name: bar
    version: 1.0.0
    repository: file://./bar
`)
	writeFile("foo/values.yaml", `
top: 1
`)
	writeFile("foo/bar/Chart.yaml", `
apiVersion: v2
name: bar
version: 1.0.0
`)
	writeFile("foo/bar/values.yaml", `
inside: 2
`)

	viper.Reset()
	viper.Set("chart-search-root", filepath.Join(tmpDir, "foo"))
	viper.Set("dry-run", false)
	viper.Set("no-dependencies", true)
	viper.Set("add-schema-reference", false)
	viper.Set("keep-full-comment", false)
	viper.Set("helm-docs-compatibility-mode", false)
	viper.Set("uncomment", false)
	viper.Set("output-file", "values.schema.json")
	viper.Set("dont-strip-helm-docs-prefix", false)
	viper.Set("append-newline", false)
	viper.Set("dependencies-filter", []string{})
	viper.Set("dont-add-global", true)
	viper.Set("skip-dependencies-schema-validation", false)
	viper.Set("allow-circular-dependencies", false)
	viper.Set("annotate", false)
	viper.Set("keep-existing-dep-schemas", false)
	viper.Set("value-files", []string{"values.yaml"})
	viper.Set("skip-auto-generation", []string{})
	viper.Set("log-level", "info")

	err := exec(nil, nil)
	assert.NoError(t, err)

	parentSchemaPath := filepath.Join(tmpDir, "foo", "values.schema.json")
	_, err = os.Stat(parentSchemaPath)
	assert.NoError(t, err, "parent chart schema must still be generated with --no-dependencies")

	depSchemaPath := filepath.Join(tmpDir, "foo", "bar", "values.schema.json")
	_, err = os.Stat(depSchemaPath)
	assert.True(t, os.IsNotExist(err),
		"dependency chart schema must not be generated with --no-dependencies (issue #215)")
}


================================================
FILE: cmd/helm-schema/version.go
================================================
package main

var version string = "0.23.2"


================================================
FILE: examples/Chart.yaml
================================================
apiVersion: v2
name: example
description: A Helm chart for Kubernetes

# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application

# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0

# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "1.16.0"


================================================
FILE: examples/values.schema.json
================================================
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "additionalProperties": false,
  "properties": {
    "global": {
      "description": "Global values are values that can be accessed from any chart or subchart by exactly the same name.",
      "required": [],
      "title": "global",
      "type": "object"
    },
    "service": {
      "additionalProperties": true,
      "properties": {
        "conf": {
          "additionalProperties": false,
          "examples": [
            "API_PROVIDER_ONE: api-key-x",
            "EMAIL_ADMIN: admin@example.org"
          ],
          "patternProperties": {
            "^API_.*": {
              "pattern": "^api-key",
              "type": "string"
            },
            "^EMAIL_.*": {
              "format": "idn-email",
              "type": "string"
            }
          },
          "required": [],
          "title": "conf",
          "type": "object"
        },
        "contact": {
          "default": "",
          "examples": [
            "name@domain.tld"
          ],
          "format": "idn-email",
          "required": [],
          "title": "contact"
        },
        "enabled": {
          "default": true,
          "description": "Type will be parsed as boolean",
          "title": "enabled",
          "type": "boolean"
        },
        "env": {
          "additionalProperties": false,
          "description": "Environment variables. If you want to provide auto-completion to the user",
          "properties": {
            "ADMIN_EMAIL": {
              "examples": [
                "admin@example.org"
              ],
              "format": "idn-email",
              "title": "ADMIN_EMAIL",
              "type": "string"
            },
            "API_URL": {
              "examples": [
                "https://api.example.org"
              ],
              "format": "idn-hostname",
              "title": "API_URL",
              "type": "string"
            },
            "CONFIG_PATH": {
              "description": "The local path to the service configuration file",
              "examples": [
                "/path/to/config"
              ],
              "title": "CONFIG_PATH",
              "type": "string"
            }
          },
          "required": [],
          "title": "env"
        },
        "externalIP": {
          "default": "443",
          "enum": [
            443,
            80
          ],
          "required": [],
          "title": "externalIP"
        },
        "hosts": {
          "description": "Will give auto-completion for the below structure\nhosts:\n - name:\n     url: my.example.org",
          "items": {
            "properties": {
              "host": {
                "properties": {
                  "url": {
                    "format": "idn-hostname",
                    "type": "string"
                  }
                },
                "required": [],
                "type": "object"
              }
            },
            "required": [],
            "type": "object"
          },
          "title": "hosts",
          "type": "array"
        },
        "maintainer": {
          "const": "maintainer@example.org",
          "default": "maintainer@example.org",
          "required": [],
          "title": "maintainer"
        },
        "name": {
          "anyOf": [
            {
              "required": []
            },
            {
              "pattern": "^foo-",
              "required": []
            }
          ],
          "default": "",
          "description": "Name of the deployed service. Defined in the schema annotation",
          "required": [],
          "title": "name"
        },
        "otherExternalIP": {
          "default": 443,
          "examples": [
            443,
            80
          ],
          "title": "otherExternalIP",
          "type": "integer"
        },
        "port": {
          "default": 80,
          "maximum": 89,
          "minimum": 80,
          "title": "port",
          "type": "integer"
        },
        "storage": {
          "default": "10Gib",
          "examples": [
            "5Gib",
            "10Gib",
            "20Gib"
          ],
          "pattern": "^[1-9][0-9]*Gib$",
          "title": "storage",
          "type": "string"
        },
        "telemetry": {
          "default": true,
          "title": "telemetry",
          "type": "boolean"
        },
        "type": {
          "default": "application",
          "enum": [
            "application",
            "controller",
            "api"
          ],
          "required": [],
          "title": "type"
        }
      },
      "required": [
        "enabled"
      ],
      "title": "service"
    }
  },
  "required": [],
  "type": "object"
}

================================================
FILE: examples/values.yaml
================================================
# vim: set ft=yaml:
# yaml-language-server: $schema=values.schema.json

# This is an example values.yaml file, it aims to show how to annotate the keys, and it's its only purpose.
# The corresponding values.schema.json has been generated with the below command.
# helm-schema -n -k additionalProperties

# @schema
# additionalProperties: true
# @schema
service:
  # Type will be parsed as boolean
  enabled: true

  # @schema
  # type: integer
  # minimum: 80
  # maximum: 89
  # @schema
  port: 80

  # @schema
  # enum: [443, 80]
  # @schema
  externalIP: 443

  # @schema
  # type: integer
  # examples: [443, 80]
  # @schema
  otherExternalIP: 443

  # @schema
  # description: Name of the deployed service. Defined in the schema annotation
  # anyOf:
  #   - type: null
  #   - pattern: ^foo-
  # @schema
  # This comment will not be parsed as 'description', the 'description' field take precedence over comments
  name:

  # @schema
  # properties:
  #   CONFIG_PATH:
  #     title: CONFIG_PATH
  #     type: string
  #     description: The local path to the service configuration file
  #     examples: [/path/to/config]
  #   ADMIN_EMAIL:
  #     title: ADMIN_EMAIL
  #     type: string
  #     format: idn-email
  #     examples: [admin@example.org]
  #   API_URL:
  #     title: API_URL
  #     type: string
  #     format: idn-hostname
  #     examples: [https://api.example.org]
  # @schema
  # -- Environment variables. If you want to provide auto-completion to the user
  env: {}

  # @schema
  # type: object
  # patternProperties:
  #   "^API_.*":
  #     type: string
  #     pattern: ^api-key
  #   "^EMAIL_.*":
  #     type: string
  #     format: idn-email
  # examples: ["API_PROVIDER_ONE: api-key-x","EMAIL_ADMIN: admin@example.org"]
  # @schema
  conf: {}

  # @schema
  # format: idn-email
  # examples: [name@domain.tld]
  # @schema
  contact: ""

  # @schema
  # type: boolean
  # default: true
  # @schema
  telemetry: true

  # @schema
  # type: array
  # items:
  #   type: object
  #   properties:
  #     host:
  #       type: object
  #       properties:
  #         url:
  #           type: string
  #           format: idn-hostname
  # @schema
  # Will give auto-completion for the below structure
  # hosts:
  #  - name:
  #      url: my.example.org
  hosts: []

  # @schema
  # enum:
  # - application
  # - controller
  # - api
  # @schema
  type: application

  # @schema
  # const: maintainer@example.org
  # @schema
  maintainer: maintainer@example.org

  # @schema
  # type: string
  # pattern: ^[1-9][0-9]*Gib$
  # examples: ["5Gib","10Gib","20Gib"]
  # @schema
  storage: "10Gib"


================================================
FILE: go.mod
================================================
module github.com/dadav/helm-schema

go 1.25.0

require (
	github.com/dadav/go-jsonpointer v0.0.0-20240918181927-335cbee8c279
	github.com/magiconair/properties v1.8.10
	github.com/norwoodj/helm-docs v1.14.2
	github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
	github.com/sirupsen/logrus v1.9.4
	github.com/spf13/cobra v1.10.2
	github.com/spf13/viper v1.21.0
	github.com/stretchr/testify v1.11.1
	gopkg.in/yaml.v3 v3.0.1
)

require (
	dario.cat/mergo v1.0.2 // indirect
	github.com/Masterminds/goutils v1.1.1 // indirect
	github.com/Masterminds/semver/v3 v3.4.0 // indirect
	github.com/Masterminds/sprig/v3 v3.3.0 // indirect
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
	github.com/fsnotify/fsnotify v1.9.0 // indirect
	github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/huandu/xstrings v1.5.0 // indirect
	github.com/inconshreveable/mousetrap v1.1.0 // indirect
	github.com/mitchellh/copystructure v1.2.0 // indirect
	github.com/mitchellh/reflectwalk v1.0.2 // indirect
	github.com/pelletier/go-toml/v2 v2.3.0 // indirect
	github.com/pkg/errors v0.9.1 // indirect
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
	github.com/sagikazarmark/locafero v0.12.0 // indirect
	github.com/shopspring/decimal v1.4.0 // indirect
	github.com/spf13/afero v1.15.0 // indirect
	github.com/spf13/cast v1.10.0 // indirect
	github.com/spf13/pflag v1.0.10 // indirect
	github.com/subosito/gotenv v1.6.0 // indirect
	go.yaml.in/yaml/v3 v3.0.4 // indirect
	golang.org/x/crypto v0.49.0 // indirect
	golang.org/x/sys v0.42.0 // indirect
	golang.org/x/text v0.35.0 // indirect
	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
	helm.sh/helm/v3 v3.20.2 // indirect
)


================================================
FILE: go.sum
================================================
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/dadav/go-jsonpointer v0.0.0-20240918181927-335cbee8c279 h1:GxxxrAMM6YJK4Tf7c6APwkhXzzpJm2hGeWKpD3OKpHY=
github.com/dadav/go-jsonpointer v0.0.0-20240918181927-335cbee8c279/go.mod h1:JoQhlwhuWFnmxzDNgByW0ONYJZjQGnUnRu9H7+C0lTU=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/norwoodj/helm-docs v1.14.2 h1:Ew3bCq1hZqMnnTopkk66Uy2mGwu/jAclAx+3JAVp1To=
github.com/norwoodj/helm-docs v1.14.2/go.mod h1:qdo76rorOkPDme8nsV5e0JBAYrs56kzvZMYW83k1kgc=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
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/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
helm.sh/helm/v3 v3.20.2 h1:binM4rvPx5DcNsa1sIt7UZi55lRbu3pZUFmQkSoRh48=
helm.sh/helm/v3 v3.20.2/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww=


================================================
FILE: install-binary.sh
================================================
#!/usr/bin/env sh

# Shamelessly copied from https://github.com/technosophos/helm-template/blob/master/install-binary.sh

PROJECT_NAME="helm-schema"
BINARY_NAME="helm-schema"
PROJECT_GH="dadav/$PROJECT_NAME"
PLUGIN_MANIFEST="plugin.yaml"

# Convert HELM_BIN and HELM_PLUGIN_DIR to unix if cygpath is
# available. This is the case when using MSYS2 or Cygwin
# on Windows where helm returns a Windows path but we
# need a Unix path

if command -v cygpath >/dev/null 2>&1; then
  HELM_BIN="$(cygpath -u "${HELM_BIN}")"
  HELM_PLUGIN_DIR="$(cygpath -u "${HELM_PLUGIN_DIR}")"
fi

[ -z "$HELM_BIN" ] && HELM_BIN=$(command -v helm)

[ -z "$HELM_HOME" ] && HELM_HOME=$(helm env | grep 'HELM_DATA_HOME' | cut -d '=' -f2 | tr -d '"')

mkdir -p "$HELM_HOME"

if [ "$SKIP_BIN_INSTALL" = "1" ]; then
  echo "Skipping binary install"
  exit
fi

# which mode is the common installer script running in.
SCRIPT_MODE="install"
if [ "$1" = "-u" ]; then
  SCRIPT_MODE="update"
fi

# initArch discovers the architecture for this system.
initArch() {
  ARCH=$(uname -m)
  case $ARCH in
  armv6*) ARCH="armv6" ;;
  armv7*) ARCH="armv7" ;;
  aarch64 | arm64) ARCH="arm64" ;;
  x86_64 | amd64) ARCH="x86_64" ;;
  *)
    echo "Arch '$(uname -m)' not supported!" >&2
    exit 1
    ;;
  esac
}

# initOS discovers the operating system for this system.
initOS() {
  OS=$(uname -s)

  case "$OS" in
  Windows_NT) OS='Windows' ;;
  # Msys support
  MSYS*) OS='Windows' ;;
  # Minimalist GNU for Windows
  MINGW*) OS='Windows' ;;
  CYGWIN*) OS='Windows' ;;
  Darwin) OS='Darwin' ;;
  Linux) OS='Linux' ;;
  *)
    echo "OS '$(uname)' not supported!" >&2
    exit 1
    ;;
  esac
}

# verifySupported checks that the os/arch combination is supported for binary builds.
verifySupported() {
  supported="Linux-x86_64\nLinux-arm64\nLinux-armv6\nLinux-armv7\nDarwin-x86_64\nDarwin-arm64\nWindows-x86_64\nWindows-arm64\nWindows-armv6\nWindows-armv7"
  if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then
    echo "No prebuild binary for ${OS}-${ARCH}."
    exit 1
  fi

  if
    ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1
  then
    echo "Either curl or wget is required"
    exit 1
  fi
}

# getDownloadURL retrieves the download URL and checksum URL.
getDownloadURL() {
  version="$(grep <"$HELM_PLUGIN_DIR/$PLUGIN_MANIFEST" "version" | cut -d '"' -f 2)"
  ext="tar.gz"
  if [ "$OS" = "Windows" ]; then
    ext="zip"
  fi
  if [ "$SCRIPT_MODE" = "install" ] && [ -n "$version" ]; then
    DOWNLOAD_URL="https://github.com/${PROJECT_GH}/releases/download/${version}/${PROJECT_NAME}_${version}_${OS}_${ARCH}.${ext}"
    CHECKSUM_URL="https://github.com/${PROJECT_GH}/releases/download/${version}/checksums.txt"
  else
    DOWNLOAD_URL="https://github.com/${PROJECT_GH}/releases/latest/download/${PROJECT_NAME}_${version}_${OS}_${ARCH}.${ext}"
    CHECKSUM_URL="https://github.com/${PROJECT_GH}/releases/latest/download/checksums.txt"
  fi
}

# Temporary dir
mkTempDir() {
  HELM_TMP="$(mktemp -d -t "${PROJECT_NAME}-XXXXXX")"
}

rmTempDir() {
  if [ -d "${HELM_TMP:-/tmp/helm-schema}" ]; then
    rm -rf "${HELM_TMP:-/tmp/helm-schema}"
  fi
}

# downloadFile downloads the latest binary package and the checksum.
downloadFile() {
  PLUGIN_TMP_FILE="${HELM_TMP}/${PROJECT_NAME}.tar.gz"
  PLUGIN_CHECKSUMS_FILE="${HELM_TMP}/${PROJECT_NAME}_checksums.txt"
  echo "Downloading ..."
  echo "$DOWNLOAD_URL"
  echo "$CHECKSUM_URL"
  if
    command -v curl >/dev/null 2>&1
  then
    curl -sSf -L "$DOWNLOAD_URL" >"$PLUGIN_TMP_FILE"
    curl -sSf -L "$CHECKSUM_URL" >"$PLUGIN_CHECKSUMS_FILE"
  elif
    command -v wget >/dev/null 2>&1
  then
    wget -q -O - "$DOWNLOAD_URL" >"$PLUGIN_TMP_FILE"
    wget -q -O - "$CHECKSUM_URL" >"$PLUGIN_CHECKSUMS_FILE"
  fi
}

validateChecksum() {
  if ! grep -q ${1} ${2}; then
    echo "Invalid checksum" >/dev/stderr
    exit 1
  fi
  echo "Checksum is valid."
}

# installFile verifies the SHA256 for the file, then unpacks and installs it.
installFile() {
  if command -v sha256sum >/dev/null 2>&1; then
    checksum=$(sha256sum ${PLUGIN_TMP_FILE} | awk '{ print $1 }')
    validateChecksum ${checksum} ${PLUGIN_CHECKSUMS_FILE}
  elif command -v openssl >/dev/null 2>&1; then
    checksum=$(openssl dgst -sha256 ${PLUGIN_TMP_FILE} | awk '{ print $2 }')
    validateChecksum ${checksum} ${PLUGIN_CHECKSUMS_FILE}
  else
    echo "WARNING: no tool found to verify checksum" >/dev/stderr
  fi

  HELM_TMP_BIN="$HELM_TMP/$BINARY_NAME"
  if [ "${OS}" = "Windows" ]; then
    HELM_TMP_BIN="$HELM_TMP_BIN.exe"
    unzip "$PLUGIN_TMP_FILE" -d "$HELM_TMP"
  else
    tar xzf "$PLUGIN_TMP_FILE" -C "$HELM_TMP"
  fi
  echo "Preparing to install into ${HELM_PLUGIN_DIR}"
  mkdir -p "$HELM_PLUGIN_DIR/bin"
  cp "$HELM_TMP_BIN" "$HELM_PLUGIN_DIR/bin"
}

# exit_trap is executed if on exit (error or not).
exit_trap() {
  result=$?
  rmTempDir
  if [ "$result" != "0" ]; then
    echo "Failed to install $PROJECT_NAME"
    printf "\tFor support, go to https://github.com/%s.\n" "$PROJECT_GH"
  fi
  exit $result
}

# testVersion tests the installed client to make sure it is working.
testVersion() {
  set +e
  echo "$PROJECT_NAME installed into $HELM_PLUGIN_DIR"
  "${HELM_PLUGIN_DIR}/bin/$BINARY_NAME" --version
  set -e
}

# Execution

#Stop execution on any error
trap "exit_trap" EXIT
set -e
initArch
initOS
verifySupported
getDownloadURL
mkTempDir
downloadFile
installFile
testVersion


================================================
FILE: pkg/chart/chart.go
================================================
package chart

import (
	"io"

	"github.com/dadav/helm-schema/pkg/util"
	yaml "gopkg.in/yaml.v3"
)

type Dependency struct {
	Name         string        `yaml:"name"`
	Version      string        `yaml:"version"`
	Condition    string        `yaml:"condition,omitempty"`
	Repository   string        `yaml:"repository,omitempty"`
	Alias        string        `yaml:"alias,omitempty"`
	ImportValues []interface{} `yaml:"import-values,omitempty"`
	// Tags         []string `yaml:"tags,omitempty"`
}

// Maintainer describes a Chart maintainer.
// https://github.com/helm/helm/blob/main/pkg/chart/metadata.go#L26C1-L34C2
type Maintainer struct {
	// Name is a user name or organization name
	Name string `json:"name,omitempty"`
	// Email is an optional email address to contact the named maintainer
	Email string `json:"email,omitempty"`
	// URL is an optional URL to an address for the named maintainer
	URL string `json:"url,omitempty"`
}

// https://github.com/helm/helm/blob/main/pkg/chart/metadata.go#L48
type ChartFile struct {
	// The name of the chart. Required.
	Name string `yaml:"name,omitempty"`
	// The URL to a relevant project page, git repo, or contact person
	Home string `yaml:"home,omitempty"`
	// Source is the URL to the source code of this chart
	Sources []string `yaml:"sources,omitempty"`
	// A SemVer 2 conformant version string of the chart. Required.
	Version string `yaml:"version,omitempty"`
	// A one-sentence description of the chart
	Description string `yaml:"description,omitempty"`
	// A list of string keywords
	Keywords []string `yaml:"keywords,omitempty"`
	// A list of name and URL/email address combinations for the maintainer(s)
	Maintainers []*Maintainer `yaml:"maintainers,omitempty"`
	// The URL to an icon file.
	Icon string `yaml:"icon,omitempty"`
	// The API Version of this chart. Required.
	APIVersion string `yaml:"apiVersion,omitempty"`
	// The condition to check to enable chart
	Condition string `yaml:"condition,omitempty"`
	// The tags to check to enable chart
	Tags string `yaml:"tags,omitempty"`
	// The version of the application enclosed inside of this chart.
	AppVersion string `yaml:"appVersion,omitempty"`
	// Whether or not this chart is deprecated
	Deprecated bool `yaml:"deprecated,omitempty"`
	// Annotations are additional mappings uninterpreted by Helm,
	// made available for inspection by other applications.
	Annotations map[string]string `yaml:"annotations,omitempty"`
	// KubeVersion is a SemVer constraint specifying the version of Kubernetes required.
	KubeVersion string `yaml:"kubeVersion,omitempty"`
	// Dependencies are a list of dependencies for a chart.
	Dependencies []*Dependency `yaml:"dependencies,omitempty"`
	// Specifies the chart type: application or library
	Type string `yaml:"type,omitempty"`
}

// ReadChart parses the given yaml into a ChartFile struct
func ReadChart(reader io.Reader) (ChartFile, error) {
	var chart ChartFile

	chartContent, err := util.ReadFileAndFixNewline(reader)
	if err != nil {
		return chart, err
	}

	err = yaml.Unmarshal(chartContent, &chart)
	if err != nil {
		return chart, err
	}
	return chart, nil
}


================================================
FILE: pkg/chart/chart_test.go
================================================
package chart

import (
	"bytes"
	"testing"

	"gopkg.in/yaml.v3"
)

func TestReadChartFile(t *testing.T) {
	data := []byte(`
  name: test
  description: test
  dependencies:
    - name: test
  `)
	r := bytes.NewReader(data)
	c, err := ReadChart(r)
	if err != nil {
		t.Errorf("Error while reading test data: %v", err)
	}
	if c.Name != "test" {
		t.Errorf("Expected Name was test, but got %v", c.Name)
	}
	if c.Description != "test" {
		t.Errorf("Expected Description was test, but got %v", c.Description)
	}
	if len(c.Dependencies) != 1 {
		t.Errorf("Expected to find one dependency, but got %d", len(c.Dependencies))
	}
	if c.Dependencies[0].Name != "test" {
		t.Errorf("Expected Dependency name was test, but got %v", c.Dependencies[0].Name)
	}
}

func TestImportValuesParsing(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		expected []interface{}
		wantErr  bool
	}{
		{
			name: "simple string import-values",
			input: `
name: dep1
import-values:
  - defaults`,
			expected: []interface{}{"defaults"},
			wantErr:  false,
		},
		{
			name: "complex map import-values",
			input: `
name: dep1
import-values:
  - child: exports.data
    parent: mydata`,
			expected: []interface{}{
				map[string]interface{}{
					"child":  "exports.data",
					"parent": "mydata",
				},
			},
			wantErr: false,
		},
		{
			name: "mixed import-values",
			input: `
name: dep1
import-values:
  - defaults
  - child: exports.config
    parent: config`,
			expected: []interface{}{
				"defaults",
				map[string]interface{}{
					"child":  "exports.config",
					"parent": "config",
				},
			},
			wantErr: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var dep Dependency
			err := yaml.Unmarshal([]byte(tt.input), &dep)

			if (err != nil) != tt.wantErr {
				t.Errorf("yaml.Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if !tt.wantErr {
				if len(dep.ImportValues) != len(tt.expected) {
					t.Errorf("ImportValues length = %v, want %v", len(dep.ImportValues), len(tt.expected))
					return
				}
				for i, val := range dep.ImportValues {
					switch expected := tt.expected[i].(type) {
					case string:
						if val != expected {
							t.Errorf("ImportValues[%d] = %v, want %v", i, val, expected)
						}
					case map[string]interface{}:
						valMap, ok := val.(map[string]interface{})
						if !ok {
							t.Errorf("ImportValues[%d] is not a map, got %T", i, val)
							continue
						}
						for k, v := range expected {
							if valMap[k] != v {
								t.Errorf("ImportValues[%d][%s] = %v, want %v", i, k, valMap[k], v)
							}
						}
					}
				}
			}
		})
	}
}

func TestChartFileParsing(t *testing.T) {
	tests := []struct {
		name     string
		input    string
		expected ChartFile
		wantErr  bool
	}{
		{
			name: "basic chart file",
			input: `
name: mychart
description: A test chart
dependencies:
  - name: dep1
    alias: aliased-dep
    condition: subchart.enabled`,
			expected: ChartFile{
				Name:        "mychart",
				Description: "A test chart",
				Dependencies: []*Dependency{
					{
						Name:      "dep1",
						Alias:     "aliased-dep",
						Condition: "subchart.enabled",
					},
				},
			},
			wantErr: false,
		},
		{
			name: "chart file without dependencies",
			input: `
name: standalone
description: A standalone chart`,
			expected: ChartFile{
				Name:        "standalone",
				Description: "A standalone chart",
			},
			wantErr: false,
		},
		{
			name: "invalid yaml",
			input: `
name: broken
description: [broken`,
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var got ChartFile
			err := yaml.Unmarshal([]byte(tt.input), &got)

			if (err != nil) != tt.wantErr {
				t.Errorf("yaml.Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
				return
			}

			if !tt.wantErr {
				if got.Name != tt.expected.Name {
					t.Errorf("Name = %v, want %v", got.Name, tt.expected.Name)
				}
				if got.Description != tt.expected.Description {
					t.Errorf("Description = %v, want %v", got.Description, tt.expected.Description)
				}
				if len(got.Dependencies) != len(tt.expected.Dependencies) {
					t.Errorf("Dependencies length = %v, want %v", len(got.Dependencies), len(tt.expected.Dependencies))
					return
				}
				for i, dep := range got.Dependencies {
					if dep.Name != tt.expected.Dependencies[i].Name {
						t.Errorf("Dependency[%d].Name = %v, want %v", i, dep.Name, tt.expected.Dependencies[i].Name)
					}
					if dep.Alias != tt.expected.Dependencies[i].Alias {
						t.Errorf("Dependency[%d].Alias = %v, want %v", i, dep.Alias, tt.expected.Dependencies[i].Alias)
					}
					if dep.Condition != tt.expected.Dependencies[i].Condition {
						t.Errorf("Dependency[%d].Condition = %v, want %v", i, dep.Condition, tt.expected.Dependencies[i].Condition)
					}
				}
			}
		})
	}
}


================================================
FILE: pkg/chart/searching/dependency_charts.go
================================================
package searching

import (
	"archive/tar"
	"compress/gzip"
	"fmt"
	"github.com/dadav/helm-schema/pkg/chart"
	"gopkg.in/yaml.v3"
	"io"
	"os"
	"path/filepath"
	"strings"
)

func extractTGZ(src, dest string) error {
	file, err := os.Open(src)
	if err != nil {
		return err
	}
	defer file.Close()

	// Open gzip reader
	gzr, err := gzip.NewReader(file)
	if err != nil {
		return err
	}
	defer gzr.Close()

	// Open tar reader
	tr := tar.NewReader(gzr)

	// Extract files
	for {
		header, err := tr.Next()
		if err == io.EOF {
			break
		}
		if err != nil {
			return err
		}

		// Resolve and sanitize file path
		cleanName := filepath.Clean(header.Name)
		// Prevent absolute paths
		if filepath.IsAbs(cleanName) {
			return fmt.Errorf("tar entry has absolute path: %s", cleanName)
		}
		// Prevent path traversal outside dest
		target := filepath.Join(dest, cleanName)
		rel, err := filepath.Rel(dest, target)
		if err != nil {
			return fmt.Errorf("failed to get relative path: %v", err)
		}
		if strings.HasPrefix(rel, "..") || rel == ".." {
			return fmt.Errorf("tar entry attempts to write outside destination: %s", cleanName)
		}

		switch header.Typeflag {
		case tar.TypeDir:
			// Create directory if not exists
			if err := os.MkdirAll(target, 0755); err != nil {
				return err
			}
		case tar.TypeReg:
			// Ensure directory exists
			if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
				return err
			}

			// Create file
			outFile, err := os.Create(target)
			if err != nil {
				return err
			}
			// Copy file content
			if _, err := io.Copy(outFile, tr); err != nil {
				outFile.Close()
				return err
			}
			if err := outFile.Close(); err != nil {
				return err
			}
		}
	}
	return nil
}

func SearchFiles(chartSearchRoot, startPath, fileName string, dependenciesFilter map[string]bool, queue chan<- string, errs chan<- error) {
	defer close(queue)
	err := filepath.Walk(startPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			errs <- err
			return nil
		}

		if !info.IsDir() && info.Name() == fileName {
			if filepath.Dir(path) == chartSearchRoot {
				queue <- path
				return nil
			}

			if len(dependenciesFilter) > 0 {
				chartData, err := os.ReadFile(path)
				if err != nil {
					errs <- fmt.Errorf("failed to read Chart.yaml at %s: %w", path, err)
					return nil
				}

				var chartFile chart.ChartFile
				if err := yaml.Unmarshal(chartData, &chartFile); err != nil {
					errs <- fmt.Errorf("failed to parse Chart.yaml at %s: %w", path, err)
					return nil
				}

				if dependenciesFilter[chartFile.Name] {
					queue <- path
				}
			} else {
				queue <- path
			}
		}

		return nil
	})
	if err != nil {
		errs <- err
	}
}

func SearchArchivesOpenTemp(startPath string, errs chan<- error) string {
	tempDir := ""
	tempDirCreationFailed := false
	err := filepath.Walk(startPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			errs <- err
			return nil
		}
		if strings.HasSuffix(info.Name(), ".tgz") || strings.HasSuffix(info.Name(), ".tar.gz") {
			// Skip extraction if temp dir creation previously failed
			if tempDirCreationFailed {
				return nil
			}
			// Extract archived charts from deps
			if tempDir == "" {
				relativeDir := filepath.Dir(path)
				var mkdirErr error
				tempDir, mkdirErr = os.MkdirTemp(relativeDir, "tmp-*")
				if mkdirErr != nil {
					errs <- fmt.Errorf("failed to create temp directory for chart extraction: %w", mkdirErr)
					tempDirCreationFailed = true
					return nil
				}
			}
			if extractErr := extractTGZ(path, tempDir); extractErr != nil {
				errs <- fmt.Errorf("failed to extract %s: %w", path, extractErr)
				return nil
			}
		}
		return nil
	})
	if err != nil {
		errs <- err
	}
	return tempDir
}


================================================
FILE: pkg/schema/annotate.go
================================================
package schema

import (
	"fmt"
	"os"
	"sort"
	"strings"

	log "github.com/sirupsen/logrus"
	"gopkg.in/yaml.v3"
)

// HasSchemaAnnotation checks if a HeadComment already contains a # @schema block.
// It matches exact "# @schema" lines but not "# @schema.root" lines.
func HasSchemaAnnotation(comment string) bool {
	for _, line := range strings.Split(comment, "\n") {
		trimmed := strings.TrimSpace(line)
		if trimmed == "# @schema" {
			return true
		}
	}
	return false
}

// typeAnnotationFromTag maps a YAML tag to the annotation type string.
// Uses the same mapping as typeFromTag.
func typeAnnotationFromTag(tag string) string {
	switch tag {
	case nullTag:
		return `"null"`
	case boolTag:
		return "boolean"
	case strTag:
		return "string"
	case intTag:
		return "integer"
	case floatTag:
		return "number"
	case timestampTag:
		return "string"
	case arrayTag:
		return "array"
	case mapTag:
		return "object"
	default:
		return ""
	}
}

// InsertionPoint represents where to insert an annotation block in the file.
type InsertionPoint struct {
	Line    int    // 1-based line number of the key node
	Indent  string // indentation string (spaces) derived from keyNode.Column
	TypeStr string // type annotation value
}

// collectInsertionPoints walks the yaml.Node tree and collects InsertionPoints
// for keys that don't already have @schema annotations.
func collectInsertionPoints(node *yaml.Node) []InsertionPoint {
	var points []InsertionPoint
	collectInsertionPointsRecursive(node, &points)
	return points
}

func collectInsertionPointsRecursive(node *yaml.Node, points *[]InsertionPoint) {
	if node == nil {
		return
	}

	switch node.Kind {
	case yaml.DocumentNode:
		for _, child := range node.Content {
			collectInsertionPointsRecursive(child, points)
		}
	case yaml.MappingNode:
		for i := 0; i < len(node.Content)-1; i += 2 {
			keyNode := node.Content[i]
			valueNode := node.Content[i+1]

			if !HasSchemaAnnotation(keyNode.HeadComment) {
				typeStr := typeAnnotationFromTag(valueNode.Tag)
				if typeStr != "" {
					indent := strings.Repeat(" ", keyNode.Column-1)
					*points = append(*points, InsertionPoint{
						Line:    keyNode.Line,
						Indent:  indent,
						TypeStr: typeStr,
					})
				}
			}

			// Recurse into mapping values for nested keys
			if valueNode.Kind == yaml.MappingNode {
				collectInsertionPointsRecursive(valueNode, points)
			}

			// Handle alias nodes that point to mappings
			if valueNode.Kind == yaml.AliasNode && valueNode.Alias != nil && valueNode.Alias.Kind == yaml.MappingNode {
				collectInsertionPointsRecursive(valueNode.Alias, points)
			}
		}
	}
}

// AnnotateContent parses YAML content, collects insertion points for keys
// that lack @schema annotations, and inserts type annotation blocks.
// Returns the modified content.
func AnnotateContent(content []byte) ([]byte, error) {
	var doc yaml.Node
	if err := yaml.Unmarshal(content, &doc); err != nil {
		return nil, fmt.Errorf("failed to parse YAML: %w", err)
	}

	points := collectInsertionPoints(&doc)
	if len(points) == 0 {
		return content, nil
	}

	lines := strings.Split(string(content), "\n")

	// Sort by line number descending so insertions don't shift earlier line numbers
	sort.Slice(points, func(i, j int) bool {
		return points[i].Line > points[j].Line
	})

	for _, pt := range points {
		// pt.Line is 1-based; convert to 0-based index
		targetIdx := pt.Line - 1
		if targetIdx < 0 || targetIdx >= len(lines) {
			continue
		}

		// Walk backwards past any existing HeadComment lines (lines starting with #)
		// to insert the annotation above existing comments
		insertIdx := targetIdx
		for insertIdx > 0 {
			candidate := strings.TrimSpace(lines[insertIdx-1])
			if strings.HasPrefix(candidate, "#") {
				insertIdx--
			} else {
				break
			}
		}

		// Build the 3 annotation lines
		annotationLines := []string{
			pt.Indent + "# @schema",
			pt.Indent + "# type: " + pt.TypeStr,
			pt.Indent + "# @schema",
		}

		// Insert at insertIdx
		newLines := make([]string, 0, len(lines)+3)
		newLines = append(newLines, lines[:insertIdx]...)
		newLines = append(newLines, annotationLines...)
		newLines = append(newLines, lines[insertIdx:]...)
		lines = newLines
	}

	return []byte(strings.Join(lines, "\n")), nil
}

// AnnotateValuesFile reads a values.yaml file, annotates unannotated keys
// with @schema type blocks, and writes the result back (or prints to stdout if dryRun).
func AnnotateValuesFile(valuesPath string, dryRun bool) error {
	fileInfo, err := os.Stat(valuesPath)
	if err != nil {
		return fmt.Errorf("failed to stat %s: %w", valuesPath, err)
	}
	perm := fileInfo.Mode().Perm()

	content, err := os.ReadFile(valuesPath)
	if err != nil {
		return fmt.Errorf("failed to read %s: %w", valuesPath, err)
	}

	annotated, err := AnnotateContent(content)
	if err != nil {
		return fmt.Errorf("failed to annotate %s: %w", valuesPath, err)
	}

	if dryRun {
		log.Infof("Annotated values for %s", valuesPath)
		fmt.Print(string(annotated))
		return nil
	}

	if err := os.WriteFile(valuesPath, annotated, perm); err != nil {
		return fmt.Errorf("failed to write %s: %w", valuesPath, err)
	}

	log.Infof("Annotated %s", valuesPath)
	return nil
}


================================================
FILE: pkg/schema/annotate_test.go
================================================
package schema

import (
	"strings"
	"testing"

	"gopkg.in/yaml.v3"
)

func TestHasSchemaAnnotation(t *testing.T) {
	tests := []struct {
		name    string
		comment string
		want    bool
	}{
		{
			name:    "empty comment",
			comment: "",
			want:    false,
		},
		{
			name:    "no schema annotation",
			comment: "# This is a normal comment",
			want:    false,
		},
		{
			name:    "has schema annotation",
			comment: "# @schema\n# type: string\n# @schema",
			want:    true,
		},
		{
			name:    "has schema.root only",
			comment: "# @schema.root\n# title: foo\n# @schema.root",
			want:    false,
		},
		{
			name:    "has both schema and schema.root",
			comment: "# @schema.root\n# title: foo\n# @schema.root\n# @schema\n# type: string\n# @schema",
			want:    true,
		},
		{
			name:    "schema prefix but not exact",
			comment: "# @schema.something",
			want:    false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := HasSchemaAnnotation(tt.comment)
			if got != tt.want {
				t.Errorf("HasSchemaAnnotation(%q) = %v, want %v", tt.comment, got, tt.want)
			}
		})
	}
}

func TestTypeAnnotationFromTag(t *testing.T) {
	tests := []struct {
		tag  string
		want string
	}{
		{"!!null", `"null"`},
		{"!!bool", "boolean"},
		{"!!str", "string"},
		{"!!int", "integer"},
		{"!!float", "number"},
		{"!!timestamp", "string"},
		{"!!seq", "array"},
		{"!!map", "object"},
		{"!!unknown", ""},
	}

	for _, tt := range tests {
		t.Run(tt.tag, func(t *testing.T) {
			got := typeAnnotationFromTag(tt.tag)
			if got != tt.want {
				t.Errorf("typeAnnotationFromTag(%q) = %q, want %q", tt.tag, got, tt.want)
			}
		})
	}
}

func TestCollectInsertionPoints(t *testing.T) {
	tests := []struct {
		name       string
		yaml       string
		wantCount  int
		wantTypes  []string
	}{
		{
			name:      "simple flat yaml",
			yaml:      "name: hello\nport: 80\nenabled: true\n",
			wantCount: 3,
			wantTypes: []string{"string", "integer", "boolean"},
		},
		{
			name:      "nested objects",
			yaml:      "service:\n  type: ClusterIP\n  port: 80\n",
			wantCount: 3,
			wantTypes: []string{"object", "string", "integer"},
		},
		{
			name:      "already annotated key",
			yaml:      "# @schema\n# type: string\n# @schema\nname: hello\nport: 80\n",
			wantCount: 1,
			wantTypes: []string{"integer"},
		},
		{
			name:      "null value",
			yaml:      "key:\n",
			wantCount: 1,
			wantTypes: []string{`"null"`},
		},
		{
			name:      "array value",
			yaml:      "items: []\n",
			wantCount: 1,
			wantTypes: []string{"array"},
		},
		{
			name:      "empty map value",
			yaml:      "config: {}\n",
			wantCount: 1,
			wantTypes: []string{"object"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var doc yaml.Node
			if err := yaml.Unmarshal([]byte(tt.yaml), &doc); err != nil {
				t.Fatalf("failed to parse YAML: %v", err)
			}
			points := collectInsertionPoints(&doc)
			if len(points) != tt.wantCount {
				t.Errorf("got %d insertion points, want %d", len(points), tt.wantCount)
				for _, p := range points {
					t.Logf("  line=%d type=%s", p.Line, p.TypeStr)
				}
			}
			for i, wantType := range tt.wantTypes {
				if i >= len(points) {
					break
				}
				if points[i].TypeStr != wantType {
					t.Errorf("point[%d].TypeStr = %q, want %q", i, points[i].TypeStr, wantType)
				}
			}
		})
	}
}

func TestAnnotateContent(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		want    string
		wantErr bool
	}{
		{
			name:  "simple unannotated file",
			input: "port: 80\n",
			want:  "# @schema\n# type: integer\n# @schema\nport: 80\n",
		},
		{
			name:  "multiple keys",
			input: "name: hello\nport: 80\n",
			want:  "# @schema\n# type: string\n# @schema\nname: hello\n# @schema\n# type: integer\n# @schema\nport: 80\n",
		},
		{
			name:  "already fully annotated",
			input: "# @schema\n# type: integer\n# @schema\nport: 80\n",
			want:  "# @schema\n# type: integer\n# @schema\nport: 80\n",
		},
		{
			name:  "partially annotated",
			input: "# @schema\n# type: string\n# @schema\nname: hello\nport: 80\n",
			want:  "# @schema\n# type: string\n# @schema\nname: hello\n# @schema\n# type: integer\n# @schema\nport: 80\n",
		},
		{
			name:  "nested objects with indentation",
			input: "service:\n  type: ClusterIP\n  port: 80\n",
			want:  "# @schema\n# type: object\n# @schema\nservice:\n  # @schema\n  # type: string\n  # @schema\n  type: ClusterIP\n  # @schema\n  # type: integer\n  # @schema\n  port: 80\n",
		},
		{
			name:  "key with existing comment - annotation goes above comment",
			input: "# This is the port\nport: 80\n",
			want:  "# @schema\n# type: integer\n# @schema\n# This is the port\nport: 80\n",
		},
		{
			name:  "empty file",
			input: "",
			want:  "",
		},
		{
			name:  "document separator preserved",
			input: "---\nport: 80\n",
			want:  "---\n# @schema\n# type: integer\n# @schema\nport: 80\n",
		},
		{
			name:  "boolean value",
			input: "enabled: true\n",
			want:  "# @schema\n# type: boolean\n# @schema\nenabled: true\n",
		},
		{
			name:  "null value",
			input: "key:\n",
			want:  "# @schema\n# type: \"null\"\n# @schema\nkey:\n",
		},
		{
			name:  "array value",
			input: "items: []\n",
			want:  "# @schema\n# type: array\n# @schema\nitems: []\n",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := AnnotateContent([]byte(tt.input))
			if (err != nil) != tt.wantErr {
				t.Fatalf("AnnotateContent() error = %v, wantErr %v", err, tt.wantErr)
			}
			if string(got) != tt.want {
				t.Errorf("AnnotateContent() mismatch\ngot:\n%s\nwant:\n%s", string(got), tt.want)
				// Show diff line by line
				gotLines := strings.Split(string(got), "\n")
				wantLines := strings.Split(tt.want, "\n")
				maxLen := len(gotLines)
				if len(wantLines) > maxLen {
					maxLen = len(wantLines)
				}
				for i := 0; i < maxLen; i++ {
					var g, w string
					if i < len(gotLines) {
						g = gotLines[i]
					}
					if i < len(wantLines) {
						w = wantLines[i]
					}
					if g != w {
						t.Errorf("  line %d: got=%q want=%q", i, g, w)
					}
				}
			}
		})
	}
}


================================================
FILE: pkg/schema/err.go
================================================
package schema

type CircularError struct {
	msg string
}

func (e *CircularError) Error() string { return e.msg }


================================================
FILE: pkg/schema/err_test.go
================================================
package schema

import "testing"

func TestCircularError(t *testing.T) {
	tests := []struct {
		name    string
		message string
		want    string
	}{
		{
			name:    "basic error message",
			message: "circular dependency detected",
			want:    "circular dependency detected",
		},
		{
			name:    "empty message",
			message: "",
			want:    "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := &CircularError{msg: tt.message}
			if got := err.Error(); got != tt.want {
				t.Errorf("CircularError.Error() = %v, want %v", got, tt.want)
			}
		})
	}
}


================================================
FILE: pkg/schema/root_schema_test.go
================================================
package schema

import (
	"strings"
	"testing"

	"gopkg.in/yaml.v3"
)

func TestGetRootSchemaFromComment(t *testing.T) {
	tests := []struct {
		name                string
		comment             string
		expectedHasData     bool
		expectedTitle       string
		expectedDescription string
		expectedRemaining   string
		expectError         bool
	}{
		{
			name: "simple root schema",
			comment: `# @schema.root
# title: My Chart
# description: A chart for testing
# @schema.root
# This is a key comment
key: value`,
			expectedHasData:     true,
			expectedTitle:       "My Chart",
			expectedDescription: "A chart for testing",
			expectedRemaining: `# This is a key comment
key: value`,
		},
		{
			name: "root schema with custom annotations",
			comment: `# @schema.root
# title: Advanced Chart
# x-custom-field: custom-value
# additionalProperties: true
# @schema.root
# Key description`,
			expectedHasData:     true,
			expectedTitle:       "Advanced Chart",
			expectedDescription: "",
			expectedRemaining:   `# Key description`,
		},
		{
			name: "no root schema",
			comment: `# @schema
# type: string
# @schema
# Just a regular comment`,
			expectedHasData: false,
			expectedRemaining: `# @schema
# type: string
# @schema
# Just a regular comment`,
		},
		{
			name: "unclosed root schema block",
			comment: `# @schema.root
# title: Unclosed
# This will fail`,
			expectError: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			schema, remaining, err := GetRootSchemaFromComment(tt.comment)

			if tt.expectError {
				if err == nil {
					t.Errorf("Expected an error but got none")
				}
				return
			}

			if err != nil {
				t.Errorf("Unexpected error: %v", err)
				return
			}

			if schema.HasData != tt.expectedHasData {
				t.Errorf("Expected HasData=%v, got %v", tt.expectedHasData, schema.HasData)
			}

			if tt.expectedHasData {
				if schema.Title != tt.expectedTitle {
					t.Errorf("Expected Title=%q, got %q", tt.expectedTitle, schema.Title)
				}
				if schema.Description != tt.expectedDescription {
					t.Errorf("Expected Description=%q, got %q", tt.expectedDescription, schema.Description)
				}
			}

			if strings.TrimSpace(remaining) != strings.TrimSpace(tt.expectedRemaining) {
				t.Errorf("Expected remaining=%q, got %q", tt.expectedRemaining, remaining)
			}
		})
	}
}

func TestYamlToSchemaWithRootAnnotations(t *testing.T) {
	tests := []struct {
		name                   string
		yamlContent            string
		expectedTitle          string
		expectedDescription    string
		expectedAdditionalProp interface{}
		expectedCustomField    interface{}
	}{
		{
			name: "basic root schema",
			yamlContent: `# @schema.root
# title: Test Chart Values
# description: Test description
# @schema.root
foo: bar`,
			expectedTitle:       "Test Chart Values",
			expectedDescription: "Test description",
		},
		{
			name: "root schema with additionalProperties",
			yamlContent: `# @schema.root
# title: Flexible Chart
# additionalProperties: true
# @schema.root
service:
  port: 8080`,
			expectedTitle:          "Flexible Chart",
			expectedAdditionalProp: true,
		},
		{
			name: "root schema with custom annotations",
			yamlContent: `# @schema.root
# title: Custom Chart
# x-helm-version: "3.0"
# @schema.root
app: myapp`,
			expectedTitle:       "Custom Chart",
			expectedCustomField: "3.0",
		},
		{
			name: "root schema with required array",
			yamlContent: `# @schema.root
# required: [keycloak, apps]
# @schema.root
keycloak:
  url: ""
apps:
  decide:
    baseUrl: ""`,
		},
		{
			name: "root schema separated by blank lines",
			yamlContent: `# @schema.root
# additionalProperties: true
# @schema.root

# @schema
# type: object
# @schema
_: {}`,
			expectedAdditionalProp: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var node yaml.Node
			err := yaml.Unmarshal([]byte(tt.yamlContent), &node)
			if err != nil {
				t.Fatalf("Failed to unmarshal YAML: %v", err)
			}

			skipConfig := &SkipAutoGenerationConfig{}
			schema, err := YamlToSchema("", &node, false, false, false, true, skipConfig, nil)
			if err != nil {
				t.Fatalf("YamlToSchema failed: %v", err)
			}

			if schema.Title != tt.expectedTitle {
				t.Errorf("Expected Title=%q, got %q", tt.expectedTitle, schema.Title)
			}

			if tt.expectedDescription != "" && schema.Description != tt.expectedDescription {
				t.Errorf("Expected Description=%q, got %q", tt.expectedDescription, schema.Description)
			}

			if tt.expectedAdditionalProp != nil {
				if schema.AdditionalProperties == nil {
					t.Errorf("Expected AdditionalProperties=%v, got nil", tt.expectedAdditionalProp)
				} else if schema.AdditionalProperties != tt.expectedAdditionalProp {
					t.Errorf("Expected AdditionalProperties=%v, got %v", tt.expectedAdditionalProp, schema.AdditionalProperties)
				}
			}

			if tt.expectedCustomField != nil {
				if schema.CustomAnnotations == nil {
					t.Errorf("Expected CustomAnnotations to contain x-helm-version, but CustomAnnotations is nil")
				} else if val, ok := schema.CustomAnnotations["x-helm-version"]; !ok {
					t.Errorf("Expected CustomAnnotations to contain x-helm-version")
				} else if val != tt.expectedCustomField {
					t.Errorf("Expected x-helm-version=%v, got %v", tt.expectedCustomField, val)
				}
			}

			if tt.name == "root schema with required array" {
				expectedRequired := []string{"keycloak", "apps"}
				if len(schema.Required.Strings) != len(expectedRequired) {
					t.Fatalf("Expected required=%v, got %v", expectedRequired, schema.Required.Strings)
				}
				for i, item := range expectedRequired {
					if schema.Required.Strings[i] != item {
						t.Fatalf("Expected required[%d]=%q, got %q", i, item, schema.Required.Strings[i])
					}
				}
			}
		})
	}
}

func TestRootSchemaDoesNotAffectKeyAnnotations(t *testing.T) {
	yamlContent := `# @schema.root
# title: Root Title
# description: Root description
# @schema.root
# @schema
# enum: [dev, prod]
# @schema
# -- Environment setting
environment: dev

service:
  port: 8080`

	var node yaml.Node
	err := yaml.Unmarshal([]byte(yamlContent), &node)
	if err != nil {
		t.Fatalf("Failed to unmarshal YAML: %v", err)
	}

	skipConfig := &SkipAutoGenerationConfig{}
	schema, err := YamlToSchema("", &node, false, false, false, true, skipConfig, nil)
	if err != nil {
		t.Fatalf("YamlToSchema failed: %v", err)
	}

	// Check root schema
	if schema.Title != "Root Title" {
		t.Errorf("Expected root Title=%q, got %q", "Root Title", schema.Title)
	}
	if schema.Description != "Root description" {
		t.Errorf("Expected root Description=%q, got %q", "Root description", schema.Description)
	}

	// Check that environment key still has its own annotations
	if schema.Properties == nil {
		t.Fatal("Expected Properties to be set")
	}

	envSchema, ok := schema.Properties["environment"]
	if !ok {
		t.Fatal("Expected environment property to exist")
	}

	if len(envSchema.Enum) != 2 {
		t.Errorf("Expected 2 enum values, got %d", len(envSchema.Enum))
	}
	if envSchema.Description != "Environment setting" {
		t.Errorf("Expected environment Description=%q, got %q", "Environment setting", envSchema.Description)
	}
}


================================================
FILE: pkg/schema/schema.go
================================================
package schema

import (
	"bufio"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"reflect"
	"regexp"
	"slices"
	"strconv"
	"strings"

	"github.com/dadav/go-jsonpointer"
	"github.com/dadav/helm-schema/pkg/util"
	"github.com/norwoodj/helm-docs/pkg/helm"
	"github.com/santhosh-tekuri/jsonschema/v6"
	log "github.com/sirupsen/logrus"
	"gopkg.in/yaml.v3"
)

// SchemaPrefix and CommentPrefix define the markers used for schema annotations in comments
const (
	SchemaPrefix     = "# @schema"
	SchemaRootPrefix = "# @schema.root"
	CommentPrefix    = "#"

	// CustomAnnotationPrefix marks custom annotations.
	// Custom annotations are extensions to the JSON Schema specification
	// See: https://json-schema.org/blog/posts/custom-annotations-will-continue
	CustomAnnotationPrefix = "x-"
)

// YAML tag constants used for type inference
const (
	nullTag      = "!!null"
	boolTag      = "!!bool"
	strTag       = "!!str"
	intTag       = "!!int"
	floatTag     = "!!float"
	timestampTag = "!!timestamp"
	arrayTag     = "!!seq"
	mapTag       = "!!map"
)

// Precompiled regex patterns for better performance
var (
	leadingCommentsRemover = regexp.MustCompile(`(?s)(?m)(?:.*\n{2,})+`)
	helmDocsTagsRemover    = regexp.MustCompile(`(?ms)(\r\n|\r|\n)?\s*@\w+(\s+--\s)?[^\n\r]*`)
	helmDocsPrefixRemover  = regexp.MustCompile(`(?m)^--\s?`)
)

// SchemaOrBool represents a JSON Schema field that can be either a boolean or a Schema object
type SchemaOrBool interface{}

// BoolOrArrayOfString represents a JSON Schema field that can be either a boolean or an array of strings
// Used primarily for the "required" field which can be either true/false or an array of required property names
type BoolOrArrayOfString struct {
	Strings []string
	Bool    bool
}

func NewBoolOrArrayOfString(arr []string, b bool) BoolOrArrayOfString {
	return BoolOrArrayOfString{
		Strings: arr,
		Bool:    b,
	}
}

func (s *BoolOrArrayOfString) UnmarshalJSON(value []byte) error {
	var multi []string
	var single bool

	if err := json.Unmarshal(value, &multi); err == nil {
		s.Strings = multi
	} else if err := json.Unmarshal(value, &single); err == nil {
		s.Bool = single
	}
	return nil
}

func (s *BoolOrArrayOfString) MarshalJSON() ([]byte, error) {
	if len(s.Strings) > 0 {
		return json.Marshal(s.Strings)
	}
	// Return empty array - the Bool field is only used internally
	// to signal that the property should be added to parent's required list
	return json.Marshal([]string{})
}

func (s *BoolOrArrayOfString) UnmarshalYAML(value *yaml.Node) error {
	var multi []string
	if value.ShortTag() == arrayTag {
		for _, v := range value.Content {
			var typeStr string
			err := v.Decode(&typeStr)
			if err != nil {
				return err
			}
			multi = append(multi, typeStr)
		}
		s.Strings = multi
	} else if value.ShortTag() == boolTag {
		var single bool
		err := value.Decode(&single)
		if err != nil {
			return err
		}
		s.Bool = single
	} else {
		return fmt.Errorf("could not unmarshal %v to slice of string or bool", value.Content)
	}
	return nil
}

type StringOrArrayOfString []string

func (s *StringOrArrayOfString) UnmarshalYAML(value *yaml.Node) error {
	var multi []string
	if value.ShortTag() == arrayTag {
		for _, v := range value.Content {
			if v.ShortTag() == nullTag {
				multi = append(multi, "null")
			} else {
				var typeStr string
				err := v.Decode(&typeStr)
				if err != nil {
					return err
				}
				multi = append(multi, typeStr)
			}
		}
		*s = multi
	} else {
		var single string
		err := value.Decode(&single)
		if err != nil {
			return err
		}
		*s = []string{single}
	}
	return nil
}

func (s *StringOrArrayOfString) UnmarshalJSON(value []byte) error {
	var multi []string
	var single string

	if err := json.Unmarshal(value, &multi); err == nil {
		*s = multi
	} else if err := json.Unmarshal(value, &single); err == nil {
		*s = []string{single}
	}
	return nil
}

func (s *StringOrArrayOfString) MarshalJSON() ([]byte, error) {
	if len(*s) == 1 {
		return json.Marshal([]string(*s)[0])
	}
	return json.Marshal([]string(*s))
}

func (s *StringOrArrayOfString) Validate() error {
	// Check if type is valid
	for _, t := range []string(*s) {
		if t != "" &&
			t != "object" &&
			t != "string" &&
			t != "integer" &&
			t != "number" &&
			t != "array" &&
			t != "null" &&
			t != "boolean" {
			return fmt.Errorf("unsupported type %s", t)
		}
	}
	return nil
}

func (s *StringOrArrayOfString) IsEmpty() bool {
	for _, t := range []string(*s) {
		if t == "" {
			return true
		}
	}
	return len(*s) == 0
}

func (s *StringOrArrayOfString) canDropRequired() bool {
	ss := *s
	return len(ss) == 1 && (ss[0] == "string" ||
		ss[0] == "number" || ss[0] == "boolean" ||
		ss[0] == "integer" || ss[0] == "null" ||
		ss[0] == "array")
}

func (s *StringOrArrayOfString) Matches(typeString string) bool {
	for _, t := range []string(*s) {
		if t == typeString {
			return true
		}
	}
	return false
}

// MarshalJSON custom marshal method for Schema. It inlines the CustomAnnotations fields
func (s *Schema) MarshalJSON() ([]byte, error) {
	// Create a map to hold all the fields
	type Alias Schema
	data := make(map[string]interface{})

	// Marshal the Schema struct (excluding CustomAnnotations)
	alias := (*Alias)(s)
	aliasJSON, err := json.Marshal(alias)
	if err != nil {
		return nil, err
	}

	// Unmarshal the JSON back into the map
	if err := json.Unmarshal(aliasJSON, &data); err != nil {
		return nil, err
	}

	// inline the CustomAnnotations fields
	for key, value := range s.CustomAnnotations {
		data[key] = value
	}

	delete(data, "CustomAnnotations")

	// Remove "required" if the schema type is not object
	if s.Type.canDropRequired() {
		delete(data, "required")
	}

	// Explicitly include const field when it was set to null
	// This handles the case where const: null in YAML should appear as "const": null in JSON
	if s.constWasSet && s.Const == nil {
		data["const"] = nil
	}

	// Marshal the final map into JSON
	return json.Marshal(data)
}

// Schema struct contains yaml tags for reading, json for writing (creating the jsonschema)
type Schema struct {
	AdditionalProperties SchemaOrBool           `yaml:"additionalProperties,omitempty" json:"additionalProperties,omitempty"`
	Default              interface{}            `yaml:"default,omitempty"              json:"default,omitempty"`
	Then                 *Schema                `yaml:"then,omitempty"                 json:"then,omitempty"`
	PatternProperties    map[string]*Schema     `yaml:"patternProperties,omitempty"    json:"patternProperties,omitempty"`
	Properties           map[string]*Schema     `yaml:"properties,omitempty"           json:"properties,omitempty"`
	If                   *Schema                `yaml:"if,omitempty"                   json:"if,omitempty"`
	Minimum              *float64               `yaml:"minimum,omitempty"              json:"minimum,omitempty"`
	MultipleOf           *float64               `yaml:"multipleOf,omitempty"           json:"multipleOf,omitempty"`
	ExclusiveMaximum     *float64               `yaml:"exclusiveMaximum,omitempty"     json:"exclusiveMaximum,omitempty"`
	Items                *Schema                `yaml:"items,omitempty"                json:"items,omitempty"`
	ExclusiveMinimum     *float64               `yaml:"exclusiveMinimum,omitempty"     json:"exclusiveMinimum,omitempty"`
	Maximum              *float64               `yaml:"maximum,omitempty"              json:"maximum,omitempty"`
	Else                 *Schema                `yaml:"else,omitempty"                 json:"else,omitempty"`
	Pattern              string                 `yaml:"pattern,omitempty"              json:"pattern,omitempty"`
	Const                interface{}            `yaml:"const,omitempty"                json:"const,omitempty"`
	ConstFromValue       bool                   `yaml:"const-from-value,omitempty"     json:"-"`
	Ref                  string                 `yaml:"$ref,omitempty"                 json:"$ref,omitempty"`
	Schema               string                 `yaml:"$schema,omitempty"              json:"$schema,omitempty"`
	Id                   string                 `yaml:"$id,omitempty"                  json:"$id,omitempty"`
	Comment              string                 `yaml:"$comment,omitempty"             json:"$comment,omitempty"`
	Format               string                 `yaml:"format,omitempty"               json:"format,omitempty"`
	Description          string                 `yaml:"description,omitempty"          json:"description,omitempty"`
	Title                string                 `yaml:"title,omitempty"                json:"title,omitempty"`
	ContentEncoding      string                 `yaml:"contentEncoding,omitempty"      json:"contentEncoding,omitempty"`
	ContentMediaType     string                 `yaml:"contentMediaType,omitempty"     json:"contentMediaType,omitempty"`
	Type                 StringOrArrayOfString  `yaml:"type,omitempty"                 json:"type,omitempty"`
	AnyOf                []*Schema              `yaml:"anyOf,omitempty"                json:"anyOf,omitempty"`
	AllOf                []*Schema              `yaml:"allOf,omitempty"                json:"allOf,omitempty"`
	OneOf                []*Schema              `yaml:"oneOf,omitempty"                json:"oneOf,omitempty"`
	Not                  *Schema                `yaml:"not,omitempty"                  json:"not,omitempty"`
	Examples             []interface{}          `yaml:"examples,omitempty"             json:"examples,omitempty"`
	Enum                 []interface{}          `yaml:"enum,omitempty"                 json:"enum,omitempty"`
	Definitions          map[string]*Schema     `yaml:"definitions,omitempty"          json:"definitions,omitempty"`
	HasData              bool                   `yaml:"-"                              json:"-"`
	Deprecated           bool                   `yaml:"deprecated,omitempty"           json:"deprecated,omitempty"`
	ReadOnly             bool                   `yaml:"readOnly,omitempty"             json:"readOnly,omitempty"`
	WriteOnly            bool                   `yaml:"writeOnly,omitempty"            json:"writeOnly,omitempty"`
	Required             BoolOrArrayOfString    `yaml:"required,omitempty"             json:"required,omitempty"`
	CustomAnnotations    map[string]interface{} `yaml:"-"                              json:",omitempty"`
	MinLength            *int                   `yaml:"minLength,omitempty"            json:"minLength,omitempty"`
	MaxLength            *int                   `yaml:"maxLength,omitempty"            json:"maxLength,omitempty"`
	MinItems             *int                   `yaml:"minItems,omitempty"             json:"minItems,omitempty"`
	MaxItems             *int                   `yaml:"maxItems,omitempty"             json:"maxItems,omitempty"`
	UniqueItems          bool                   `yaml:"uniqueItems,omitempty"          json:"uniqueItems,omitempty"`
	Contains             *Schema                `yaml:"contains,omitempty"             json:"contains,omitempty"`
	AdditionalItems      SchemaOrBool           `yaml:"additionalItems,omitempty"      json:"additionalItems,omitempty"`
	MinProperties        *int                   `yaml:"minProperties,omitempty"        json:"minProperties,omitempty"`
	MaxProperties        *int                   `yaml:"maxProperties,omitempty"        json:"maxProperties,omitempty"`
	PropertyNames        *Schema                `yaml:"propertyNames,omitempty"        json:"propertyNames,omitempty"`
	Dependencies         map[string]interface{} `yaml:"dependencies,omitempty"         json:"dependencies,omitempty"`
	constWasSet          bool                   `yaml:"-"                              json:"-"`
}

func NewSchema(schemaType string) *Schema {
	if schemaType == "" {
		return &Schema{}
	}

	return &Schema{
		Type:     []string{schemaType},
		Required: NewBoolOrArrayOfString([]string{}, false),
	}
}

// getJsonKeys returns a slice of all JSON tag values from the Schema struct fields.
// This is used to identify known fields during YAML unmarshaling to separate them
// from custom annotations.
func (s Schema) getJsonKeys() []string {
	result := []string{}
	t := reflect.TypeOf(s)

	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		result = append(result, field.Tag.Get("json"))
	}
	return result
}

// UnmarshalYAML implements custom YAML unmarshaling for Schema objects.
// It handles both standard schema fields and custom annotations (prefixed with "x-").
// Custom annotations are stored in the CustomAnnotations map while standard fields
// are unmarshaled directly into the Schema struct.
func (s *Schema) UnmarshalYAML(node *yaml.Node) error {
	// Create an alias type to avoid recursion
	type schemaAlias Schema
	alias := new(schemaAlias)
	// copy all existing fields
	*alias = schemaAlias(*s)

	// Unmarshal known fields into alias
	if err := node.Decode(alias); err != nil {
		return err
	}

	// Initialize CustomAnnotations map
	alias.CustomAnnotations = make(map[string]interface{})

	knownKeys := s.getJsonKeys()

	// Iterate through all node fields
	for i := 0; i < len(node.Content)-1; i += 2 {
		keyNode := node.Content[i]
		valueNode := node.Content[i+1]
		key := keyNode.Value

		// Track if const field was explicitly set (even to null)
		if key == "const" {
			alias.constWasSet = true
		}

		if slices.Contains(knownKeys, key) {
			continue
		}

		// Unmarshal unknown fields into the CustomAnnotations map
		if !strings.HasPrefix(key, CustomAnnotationPrefix) {
			continue
		}
		var value interface{}
		if err := valueNode.Decode(&value); err != nil {
			return err
		}
		alias.CustomAnnotations[key] = value
	}

	// Copy alias to the main struct
	*s = Schema(*alias)
	return nil
}

// UnmarshalJSON implements custom JSON unmarshaling for Schema objects.
// It handles "definitions" (Draft 7), "$defs" (Draft 2019-09+), custom
// annotations (prefixed with "x-"), and tracks explicit "const" fields.
func (s *Schema) UnmarshalJSON(data []byte) error {
	// Create an alias type to avoid recursion
	type schemaAlias Schema
	alias := new(schemaAlias)

	// Unmarshal known fields into alias
	if err := json.Unmarshal(data, alias); err != nil {
		return err
	}

	// Parse raw JSON to check for $defs, custom annotations, and const
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(data, &raw); err != nil {
		return err
	}

	// Handle $defs (Draft 2019-09+) - merge into Definitions
	if defsRaw, ok := raw["$defs"]; ok {
		var defs map[string]*Schema
		if err := json.Unmarshal(defsRaw, &defs); err != nil {
			return fmt.Errorf("failed to unmarshal $defs: %w", err)
		}
		// Merge $defs into Definitions
		if alias.Definitions == nil {
			alias.Definitions = make(map[string]*Schema)
		}
		for k, v := range defs {
			if _, exists := alias.Definitions[k]; !exists {
				alias.Definitions[k] = v
			}
		}
	}

	// Track if const field was explicitly set (even to null)
	if _, ok := raw["const"]; ok {
		alias.constWasSet = true
	}

	// Extract custom annotations (x-* prefixed keys)
	knownKeys := s.getJsonKeys()
	alias.CustomAnnotations = make(map[string]interface{})
	for key, rawValue := range raw {
		if !strings.HasPrefix(key, CustomAnnotationPrefix) {
			continue
		}
		if slices.Contains(knownKeys, key) {
			continue
		}
		var value interface{}
		if err := json.Unmarshal(rawValue, &value); err != nil {
			return fmt.Errorf("failed to unmarshal custom annotation %s: %w", key, err)
		}
		alias.CustomAnnotations[key] = value
	}

	// Copy alias to the main struct
	*s = Schema(*alias)

	// Rewrite $ref paths from $defs to definitions for Draft 7 compatibility
	s.rewriteDefsRefs()

	return nil
}

// HoistDefinitions collects all definitions from nested schemas and hoists them
// to the root level. This is necessary because $ref paths like "#/definitions/X"
// always reference the document root, not the local schema.
func (s *Schema) HoistDefinitions() {
	if s == nil {
		return
	}

	// Initialize root definitions if needed
	if s.Definitions == nil {
		s.Definitions = make(map[string]*Schema)
	}

	// Collect definitions from all nested schemas
	s.collectAndHoistDefinitions(s.Definitions)
}

// collectAndHoistDefinitions recursively collects definitions from nested schemas
// and adds them to the root definitions map, then removes them from nested positions.
func (s *Schema) collectAndHoistDefinitions(rootDefs map[string]*Schema) {
	if s == nil {
		return
	}

	// Process nested properties
	for _, prop := range s.Properties {
		prop.collectAndHoistDefinitions(rootDefs)
		// Hoist definitions from this property
		if prop.Definitions != nil {
			for name, def := range prop.Definitions {
				if _, exists := rootDefs[name]; !exists {
					rootDefs[name] = def
				}
			}
			prop.Definitions = nil // Remove from nested position
		}
	}

	// Process pattern properties
	for _, prop := range s.PatternProperties {
		prop.collectAndHoistDefinitions(rootDefs)
		if prop.Definitions != nil {
			for name, def := range prop.Definitions {
				if _, exists := rootDefs[name]; !exists {
					rootDefs[name] = def
				}
			}
			prop.Definitions = nil
		}
	}

	// Process items
	if s.Items != nil {
		s.Items.collectAndHoistDefinitions(rootDefs)
		if s.Items.Definitions != nil {
			for name, def := range s.Items.Definitions {
				if _, exists := rootDefs[name]; !exists {
					rootDefs[name] = def
				}
			}
			s.Items.Definitions = nil
		}
	}

	// Process composition schemas
	for _, schema := range s.AllOf {
		schema.collectAndHoistDefinitions(rootDefs)
		if schema.Definitions != nil {
			for name, def := range schema.Definitions {
				if _, exists := rootDefs[name]; !exists {
					rootDefs[name] = def
				}
			}
			schema.Definitions = nil
		}
	}
	for _, schema := range s.AnyOf {
		schema.collectAndHoistDefinitions(rootDefs)
		if schema.Definitions != nil {
			for name, def := range schema.Definitions {
				if _, exists := rootDefs[name]; !exists {
					rootDefs[name] = def
				}
			}
			schema.Definitions = nil
		}
	}
	for _, schema := range s.OneOf {
		schema.collectAndHoistDefinitions(rootDefs)
		if schema.Definitions != nil {
			for name, def := range schema.Definitions {
				if _, exists := rootDefs[name]; !exists {
					rootDefs[name] = def
				}
			}
			schema.Definitions = nil
		}
	}

	// Process conditional schemas
	if s.If != nil {
		s.If.collectAndHoistDefinitions(rootDefs)
		if s.If.Definitions != nil {
			for name, def := range s.If.Definitions {
				if _, exists := rootDefs[name]; !exists {
					rootDefs[name] = def
				}
			}
			s.If.Definitions = nil
		}
	}
	if s.Then != nil {
		s.Then.collectAndHoistDefinitions(rootDefs)
		if s.Then.Definitions != nil {
			for name, def := range s.Then.Definitions {
				if _, exists := rootDefs[name]; !exists {
					rootDefs[name] = def
				}
			}
			s.Then.Definitions = nil
		}
	}
	if s.Else != nil {
		s.Else.collectAndHoistDefinitions(rootDefs)
		if s.Else.Definitions != nil {
			for name, def := range s.Else.Definitions {
				if _, exists := rootDefs[name]; !exists {
					rootDefs[name] = def
				}
			}
			s.Else.Definitions = nil
		}
	}
	if s.Not != nil {
		s.Not.collectAndHoistDefinitions(rootDefs)
		if s.Not.Definitions != nil {
			for name, def := range s.Not.Definitions {
				if _, exists := rootDefs[name]; !exists {
					rootDefs[name] = def
				}
			}
			s.Not.Definitions = nil
		}
	}

	// Process AdditionalProperties when it's a schema
	if s.AdditionalProperties != nil {
		switch v := s.AdditionalProperties.(type) {
		case *Schema:
			v.collectAndHoistDefinitions(rootDefs)
			if v.Definitions != nil {
				for name, def := range v.Definitions {
					if _, exists := rootDefs[name]; !exists {
						rootDefs[name] = def
					}
				}
				v.Definitions = nil
			}
		}
	}
}

// rewriteDefsRefs recursively rewrites $ref paths from "#/$defs/" to "#/definitions/"
// for JSON Schema Draft 7 compatibility.
func (s *Schema) rewriteDefsRefs() {
	if s == nil {
		return
	}

	// Rewrite main $ref
	if strings.HasPrefix(s.Ref, "#/$defs/") {
		s.Ref = strings.Replace(s.Ref, "#/$defs/", "#/definitions/", 1)
	}

	// Recursively process all nested schemas
	for _, prop := range s.Properties {
		prop.rewriteDefsRefs()
	}
	for _, prop := range s.PatternProperties {
		prop.rewriteDefsRefs()
	}
	for _, def := range s.Definitions {
		def.rewriteDefsRefs()
	}
	if s.Items != nil {
		s.Items.rewriteDefsRefs()
	}
	if s.Contains != nil {
		s.Contains.rewriteDefsRefs()
	}
	if s.PropertyNames != nil {
		s.PropertyNames.rewriteDefsRefs()
	}
	if s.If != nil {
		s.If.rewriteDefsRefs()
	}
	if s.Then != nil {
		s.Then.rewriteDefsRefs()
	}
	if s.Else != nil {
		s.Else.rewriteDefsRefs()
	}
	if s.Not != nil {
		s.Not.rewriteDefsRefs()
	}
	for _, schema := range s.AllOf {
		schema.rewriteDefsRefs()
	}
	for _, schema := range s.AnyOf {
		schema.rewriteDefsRefs()
	}
	for _, schema := range s.OneOf {
		schema.rewriteDefsRefs()
	}

	// Handle AdditionalProperties and AdditionalItems when they are schemas
	if s.AdditionalProperties != nil {
		switch v := s.AdditionalProperties.(type) {
		case *Schema:
			v.rewriteDefsRefs()
		case Schema:
			v.rewriteDefsRefs()
			s.AdditionalProperties = v
		}
	}
	if s.AdditionalItems != nil {
		switch v := s.AdditionalItems.(type) {
		case *Schema:
			v.rewriteDefsRefs()
		case Schema:
			v.rewriteDefsRefs()
			s.AdditionalItems = v
		}
	}
}

// Set sets the HasData field to true
func (s *Schema) Set() {
	s.HasData = true
}

// DisableRequiredProperties recursively disables all required property validations throughout the schema.
// This includes:
// - Setting the root schema's required field to an empty array
// - Recursively disabling required properties in all nested schemas (properties, items, etc.)
// - Handling all conditional schemas (if/then/else)
// - Processing all composition schemas (anyOf/oneOf/allOf)
func (s *Schema) DisableRequiredProperties() {
	s.Required = NewBoolOrArrayOfString([]string{}, false)
	for _, v := range s.Properties {
		v.DisableRequiredProperties()
	}
	if s.Items != nil {
		s.Items.DisableRequiredProperties()
	}

	if s.AnyOf != nil {
		for _, v := range s.AnyOf {
			v.DisableRequiredProperties()
		}
	}
	if s.OneOf != nil {
		for _, v := range s.OneOf {
			v.DisableRequiredProperties()
		}
	}
	if s.AllOf != nil {
		for _, v := range s.AllOf {
			v.DisableRequiredProperties()
		}
	}
	if s.If != nil {
		s.If.DisableRequiredProperties()
	}
	if s.Else != nil {
		s.Else.DisableRequiredProperties()
	}
	if s.Then != nil {
		s.Then.DisableRequiredProperties()
	}
	if s.Not != nil {
		s.Not.DisableRequiredProperties()
	}

	// Add handling for AdditionalProperties when it's a Schema
	if s.AdditionalProperties != nil {
		switch v := s.AdditionalProperties.(type) {
		case *Schema:
			v.DisableRequiredProperties()
		case Schema:
			v.DisableRequiredProperties()
			s.AdditionalProperties = v
		}
	}

	// Handle Contains schema
	if s.Contains != nil {
		s.Contains.DisableRequiredProperties()
	}

	// Handle PropertyNames schema
	if s.PropertyNames != nil {
		s.PropertyNames.DisableRequiredProperties()
	}

	// Handle AdditionalItems when it's a Schema
	if s.AdditionalItems != nil {
		switch v := s.AdditionalItems.(type) {
		case *Schema:
			v.DisableRequiredProperties()
		case Schema:
			v.DisableRequiredProperties()
			s.AdditionalItems = v
		}
	}

	// Handle Definitions
	for _, v := range s.Definitions {
		v.DisableRequiredProperties()
	}
}

// GetPropertyAtPath navigates a dot-separated path and returns the schema at that location.
// Returns nil if any part of the path doesn't exist. Empty path segments are skipped.
func (s *Schema) GetPropertyAtPath(path string) *Schema {
	if s == nil || path == "" {
		return s
	}

	parts := strings.Split(path, ".")
	current := s

	for _, part := range parts {
		if part == "" {
			continue // Skip empty segments (e.g., "foo..bar" or ".foo")
		}
		if current.Properties == nil {
			return nil
		}
		prop, ok := current.Properties[part]
		if !ok {
			return nil
		}
		current = prop
	}

	return current
}

// SetPropertyAtPath navigates a dot-separated path and ensures all intermediate schemas exist.
// Creates intermediate object schemas as needed. Returns the schema at the final path location.
// If the path is empty, returns the current schema. Empty path segments are skipped.
func (s *Schema) SetPropertyAtPath(path string) *Schema {
	if s == nil || path == "" {
		return s
	}

	parts := strings.Split(path, ".")
	current := s

	for _, part := range parts {
		if part == "" {
			continue // Skip empty segments (e.g., "foo..bar" or ".foo")
		}
		if current.Properties == nil {
			current.Properties = make(map[string]*Schema)
		}
		if _, ok := current.Properties[part]; !ok {
			current.Properties[part] = &Schema{
				Type:       []string{"object"},
				Title:      part,
				Properties: make(map[string]*Schema),
			}
		}
		current = current.Properties[part]
	}

	return current
}

// ToJson converts the data to raw json
func (s Schema) ToJson() ([]byte, error) {
	res, err := json.MarshalIndent(&s, "", "  ")
	if err != nil {
		return nil, err
	}
	return res, nil
}

// Supported format values according to JSON Schema specification
const (
	FormatDateTime       = "date-time"
	FormatTime           = "time"
	FormatDate           = "date"
	FormatDuration       = "duration"
	FormatEmail          = "email"
	FormatIDNEmail       = "idn-email"
	FormatHostname       = "hostname"
	FormatIDNHostname    = "idn-hostname"
	FormatIPv4           = "ipv4"
	FormatIPv6           = "ipv6"
	FormatUUID           = "uuid"
	FormatURI            = "uri"
	FormatURIReference   = "uri-reference"
	FormatIRI            = "iri"
	FormatIRIReference   = "iri-reference"
	FormatURITemplate    = "uri-template"
	FormatJSONPointer    = "json-pointer"
	FormatRelJSONPointer = "relative-json-pointer"
	FormatRegex          = "regex"
)

var supportedFormats = map[string]bool{
	FormatDateTime: true, FormatTime: true, FormatDate: true,
	FormatDuration: true, FormatEmail: true, FormatIDNEmail: true,
	FormatHostname: true, FormatIDNHostname: true, FormatIPv4: true,
	FormatIPv6: true, FormatUUID: true, FormatURI: true,
	FormatURIReference: true, FormatIRI: true, FormatIRIReference: true,
	FormatURITemplate: true, FormatJSONPointer: true,
	FormatRelJSONPointer: true, FormatRegex: true,
}

// Validate performs comprehensive validation of the schema
func (s Schema) Validate() error {
	// Validate schema syntax
	if err := s.validateSchemaSyntax(); err != nil {
		return err
	}

	// Validate type constraints
	if err := s.validateTypeConstraints(); err != nil {
		return err
	}

	// Validate numeric constraints
	if err := s.validateNumericConstraints(); err != nil {
		return err
	}

	// Validate string constraints
	if err := s.validateStringConstraints(); err != nil {
		return err
	}

	// Validate array constraints
	if err := s.validateArrayConstraints(); err != nil {
		return err
	}

	// Validate object constraints
	if err := s.validateObjectConstraints(); err != nil {
		return err
	}

	// Validate nested schemas
	if err := s.validateNestedSchemas(); err != nil {
		return err
	}

	return nil
}

func (s Schema) validateSchemaSyntax() error {
	jsonStr, err := s.ToJson()
	if err != nil {
		return fmt.Errorf("failed to convert schema to JSON: %w", err)
	}

	c := jsonschema.NewCompiler()
	if err := c.AddResource("schema.json", jsonStr); err != nil {
		return fmt.Errorf("invalid schema syntax: %w", err)
	}

	return s.Type.Validate()
}

func (s Schema) validateTypeConstraints() error {
	return nil
}

func (s Schema) validateNumericConstraints() error {
	if !s.hasNumericConstraints() {
		return nil
	}

	if !s.Type.IsEmpty() && !s.Type.Matches("number") && !s.Type.Matches("integer") {
		return fmt.Errorf("numeric constraints can only be used with number or integer types, got %v", s.Type)
	}

	if s.MultipleOf != nil && *s.MultipleOf <= 0 {
		return errors.New("multipleOf must be greater than 0")
	}

	if s.Minimum != nil && s.ExclusiveMinimum != nil {
		return errors.New("cannot use both minimum and exclusiveMinimum")
	}

	if s.Maximum != nil && s.ExclusiveMaximum != nil {
		return errors.New("cannot use both maximum and exclusiveMaximum")
	}

	// Validate min <= max when both are specified
	if s.Minimum != nil && s.Maximum != nil && *s.Minimum > *s.Maximum {
		return fmt.Errorf("minimum (%v) cannot be greater than maximum (%v)", *s.Minimum, *s.Maximum)
	}

	if s.ExclusiveMinimum != nil && s.ExclusiveMaximum != nil && *s.ExclusiveMinimum >= *s.ExclusiveMaximum {
		return fmt.Errorf("exclusiveMinimum (%v) must be less than exclusiveMaximum (%v)", *s.ExclusiveMinimum, *s.ExclusiveMaximum)
	}

	return nil
}

func (s Schema) validateStringConstraints() error {
	if s.Format != "" {
		if !s.Type.IsEmpty() && !s.Type.Matches("string") {
			return fmt.Errorf("format can only be used with string type, got %v", s.Type)
		}

		if !supportedFormats[s.Format] {
			return fmt.Errorf("unsupported format: %s", s.Format)
		}
	}

	if s.Pattern != "" {
		if !s.Type.IsEmpty() && !s.Type.Matches("string") {
			return fmt.Errorf("pattern can only be used with string type, got %v", s.Type)
		}
		// Validate that pattern is a valid regex
		if _, err := regexp.Compile(s.Pattern); err != nil {
			return fmt.Errorf("invalid pattern regex: %w", err)
		}
	}

	if s.Format != "" && s.Pattern != "" {
		return errors.New("cannot use both format and pattern in the same schema")
	}

	// Validate minLength/maxLength are only used with string type
	if s.MinLength != nil || s.MaxLength != nil {
		if !s.Type.IsEmpty() && !s.Type.Matches("string") {
			return fmt.Errorf("minLength/maxLength can only be used with string type, got %v", s.Type)
		}
	}

	if s.MaxLength != nil && s.MinLength != nil && *s.MinLength > *s.MaxLength {
		return fmt.Errorf("minLength (%d) cannot be greater than maxLength (%d)", *s.MinLength, *s.MaxLength)
	}

	if s.MinLength != nil && *s.MinLength < 0 {
		return errors.New("minLength must be non-negative")
	}

	if s.MaxLength != nil && *s.MaxLength < 0 {
		return errors.New("maxLength must be non-negative")
	}

	// Validate contentEncoding and contentMediaType are only used with string type
	if s.ContentEncoding != "" || s.ContentMediaType != "" {
		if !s.Type.IsEmpty() && !s.Type.Matches("string") {
			return fmt.Errorf("contentEncoding/contentMediaType can only be used with string type, got %v", s.Type)
		}
	}

	return nil
}

func (s Schema) validateArrayConstraints() error {
	if s.Items != nil {
		if !s.Type.IsEmpty() && !s.Type.Matches("array") {
			return fmt.Errorf("items can only be used with array type, got %v", s.Type)
		}

		if err := s.Items.Validate(); err != nil {
			return fmt.Errorf("invalid items schema: %w", err)
		}
	}

	if s.MinItems != nil || s.MaxItems != nil {
		if !s.Type.IsEmpty() && !s.Type.Matches("array") {
			return fmt.Errorf("minItems/maxItems can only be used with array type, got %v", s.Type)
		}

		if s.MinItems != nil && s.MaxItems != nil && *s.MaxItems < *s.MinItems {
			return fmt.Errorf("maxItems (%d) cannot be less than minItems (%d)", *s.MaxItems, *s.MinItems)
		}
	}

	if s.MinItems != nil && *s.MinItems < 0 {
		return errors.New("minItems must be non-negative")
	}

	if s.MaxItems != nil && *s.MaxItems < 0 {
		return errors.New("maxItems must be non-negative")
	}

	// Note: uniqueItems is a boolean that doesn't require type validation.
	// Per JSON Schema spec, keywords are ignored if the type doesn't match.

	// Validate contains
	if s.Contains != nil {
		if !s.Type.IsEmpty() && !s.Type.Matches("array") {
			return fmt.Errorf("contains can only be used with array type, got %v", s.Type)
		}
		if err := s.Contains.Validate(); err != nil {
			return fmt.Errorf("invalid contains schema: %w", err)
		}
	}

	// Validate additionalItems
	if s.AdditionalItems != nil {
		if !s.Type.IsEmpty() && !s.Type.Matches("array") {
			return fmt.Errorf("additionalItems can only be used with array type, got %v", s.Type)
		}
		switch v := s.AdditionalItems.(type) {
		case *Schema:
			if err := v.Validate(); err != nil {
				return fmt.Errorf("invalid additionalItems schema: %w", err)
			}
		case Schema:
			if err := v.Validate(); err != nil {
				return fmt.Errorf("invalid additionalItems schema: %w", err)
			}
		case bool:
			// Boolean is valid
		case map[string]interface{}:
			// When unmarshaled from YAML, a schema object becomes map[string]interface{}
			// Convert and validate it
			schemaBytes, err := json.Marshal(v)
			if err != nil {
				return fmt.Errorf("invalid additionalItems schema: %w", err)
			}
			var itemsSchema Schema
			if err := json.Unmarshal(schemaBytes, &itemsSchema); err != nil {
				return fmt.Errorf("invalid additionalItems schema: %w", err)
			}
			if err := itemsSchema.Validate(); err != nil {
				return fmt.Errorf("invalid additionalItems schema: %w", err)
			}
		default:
			return fmt.Errorf("additionalItems must be a boolean or schema, got %T", s.AdditionalItems)
		}
	}

	return nil
}

func (s Schema) validateObjectConstraints() error {
	// Validate minProperties/maxProperties
	if s.MinProperties != nil || s.MaxProperties != nil {
		if !s.Type.IsEmpty() && !s.Type.Matches("object") {
			return fmt.Errorf("minProperties/maxProperties can only be used with object type, got %v", s.Type)
		}

		if s.MinProperties != nil && *s.MinProperties < 0 {
			return errors.New("minProperties must be non-negative")
		}

		if s.MaxProperties != nil && *s.MaxProperties < 0 {
			return errors.New("maxProperties must be non-negative")
		}

		if s.MinProperties != nil && s.MaxProperties != nil && *s.MaxProperties < *s.MinProperties {
			return fmt.Errorf("maxProperties (%d) cannot be less than minProperties (%d)", *s.MaxProperties, *s.MinProperties)
		}
	}

	// Validate propertyNames
	if s.PropertyNames != nil {
		if !s.Type.IsEmpty() && !s.Type.Matches("object") {
			return fmt.Errorf("propertyNames can only be used with object type, got %v", s.Type)
		}
		if err := s.PropertyNames.Validate(); err != nil {
			return fmt.Errorf("invalid propertyNames schema: %w", err)
		}
	}

	// Validate additionalProperties type check
	if s.AdditionalProperties != nil {
		if !s.Type.IsEmpty() && !s.Type.Matches("object") {
			return fmt.Errorf("additionalProperties can only be used with object type, got %v", s.Type)
		}
		switch v := s.AdditionalProperties.(type) {
		case *Schema:
			if err := v.Validate(); err != nil {
				return fmt.Errorf("invalid additionalProperties schema: %w", err)
			}
		case Schema:
			if err := v.Validate(); err != nil {
				return fmt.Errorf("invalid additionalProperties schema: %w", err)
			}
		case bool:
			// Boolean is valid
		case map[string]interface{}:
			// When unmarshaled from YAML, a schema object becomes map[string]interface{}
			// Convert and validate it
			schemaBytes, err := json.Marshal(v)
			if err != nil {
				return fmt.Errorf("invalid additionalProperties schema: %w", err)
			}
			var propsSchema Schema
			if err := json.Unmarshal(schemaBytes, &propsSchema); err != nil {
				return fmt.Errorf("invalid additionalProperties schema: %w", err)
			}
			if err := propsSchema.Validate(); err != nil {
				return fmt.Errorf("invalid additionalProperties schema: %w", err)
			}
		default:
			return fmt.Errorf("additionalProperties must be a boolean or schema, got %T", s.AdditionalProperties)
		}
	}

	// Validate patternProperties patterns
	for pattern, patternSchema := range s.PatternProperties {
		if _, err := regexp.Compile(pattern); err != nil {
			return fmt.Errorf("invalid pattern in patternProperties '%s': %w", pattern, err)
		}
		if patternSchema != nil {
			if err := patternSchema.Validate(); err != nil {
				return fmt.Errorf("invalid schema in patternProperties[%s]: %w", pattern, err)
			}
		}
	}

	// Validate dependencies
	for depKey, depValue := range s.Dependencies {
		switch v := depValue.(type) {
		case []interface{}:
			// Array of property names - validate they are strings
			for i, item := range v {
				if _, ok := item.(string); !ok {
					return fmt.Errorf("dependencies[%s][%d] must be a string, got %T", depKey, i, item)
				}
			}
		case map[string]interface{}:
			// Schema - convert and validate
			schemaBytes, err := json.Marshal(v)
			if err != nil {
				return fmt.Errorf("invalid schema in dependencies[%s]: %w", depKey, err)
			}
			var depSchema Schema
			if err := json.Unmarshal(schemaBytes, &depSchema); err != nil {
				return fmt.Errorf("invalid schema in dependencies[%s]: %w", depKey, err)
			}
			if err := depSchema.Validate(); err != nil {
				return fmt.Errorf("invalid schema in dependencies[%s]: %w", depKey, err)
			}
		default:
			return fmt.Errorf("dependencies[%s] must be an array of strings or a schema, got %T", depKey, depValue)
		}
	}

	return nil
}

func (s Schema) validateNestedSchemas() error {
	// Validate combinatorial schemas
	for _, schemas := range [][]*Schema{s.AllOf, s.AnyOf, s.OneOf} {
		for _, schema := range schemas {
			if err := schema.Validate(); err != nil {
				return err
			}
		}
	}

	// Validate conditional schemas
	for _, schema := range []*Schema{s.If, s.Then, s.Else, s.Not} {
		if schema != nil {
			if err := schema.Validate(); err != nil {
				return err
			}
		}
	}

	// Validate definitions
	for name, defSchema := range s.Definitions {
		if defSchema != nil {
			if err := defSchema.Validate(); err != nil {
				return fmt.Errorf("invalid schema in definitions[%s]: %w", name, err)
			}
		}
	}

	// Validate nested properties
	for name, propSchema := range s.Properties {
		if propSchema != nil {
			if err := propSchema.Validate(); err != nil {
				return fmt.Errorf("invalid schema in properties[%s]: %w", name, err)
			}
		}
	}

	return nil
}

func (s Schema) hasNumericConstraints() bool {
	return s.Minimum != nil || s.Maximum != nil ||
		s.ExclusiveMinimum != nil || s.ExclusiveMaximum != nil ||
		s.MultipleOf != nil
}

var possibleSkipFields = []string{"type", "title", "description", "required", "default", "additionalProperties"}

type SkipAutoGenerationConfig struct {
	Type, Title, Description, Required, Default, AdditionalProperties bool
}

func NewSkipAutoGenerationConfig(flag []string) (*SkipAutoGenerationConfig, error) {
	var config SkipAutoGenerationConfig

	var invalidFlags []string

	for _, fieldName := range flag {
		if !slices.Contains(possibleSkipFields, fieldName) {
			invalidFlags = append(invalidFlags, fieldName)
		}
		if fieldName == "type" {
			config.Type = true
		}
		if fieldName == "title" {
			config.Title = true
		}
		if fieldName == "description" {
			config.Description = true
		}
		if fieldName == "required" {
			config.Required = true
		}
		if fieldName == "default" {
			config.Default = true
		}
		if fieldName == "additionalProperties" {
			config.AdditionalProperties = true
		}
	}

	if len(invalidFlags) != 0 {
		return nil, fmt.Errorf("unsupported field names '%s' for skipping auto-generation", strings.Join(invalidFlags, "', '"))
	}

	return &config, nil
}

func typeFromTag(tag string) ([]string, error) {
	switch tag {
	case nullTag:
		return []string{"null"}, nil
	case boolTag:
		return []string{"boolean"}, nil
	case strTag:
		return []string{"string"}, nil
	case intTag:
		return []string{"integer"}, nil
	case floatTag:
		return []string{"number"}, nil
	case timestampTag:
		return []string{"string"}, nil
	case arrayTag:
		return []string{"array"}, nil
	case mapTag:
		return []string{"object"}, nil
	}
	return []string{}, fmt.Errorf("unsupported yaml tag found: %s", tag)
}

// FixRequiredProperties iterates over the properties and checks if required has a boolean value.
// Then the property is added to the parents required property list
func FixRequiredProperties(schema *Schema) error {
	if schema.Properties != nil {
		for propName, propValue := range schema.Properties {
			FixRequiredProperties(propValue)
			if propValue.Required.Bool && !slices.Contains(schema.Required.Strings, propName) {
				schema.Required.Strings = append(schema.Required.Strings, propName)
			}
		}
		if !slices.Contains(schema.Type, "object") {
			// If .Properties is set, type must be object
			if len(schema.Type) == 0 {
				schema.Type = []string{"object"}
			} else {
				schema.Type = append(schema.Type, "object")
			}
		}
	}

	if schema.Then != nil {
		FixRequiredProperties(schema.Then)
	}

	if schema.If != nil {
		FixRequiredProperties(schema.If)
	}

	if schema.Else != nil {
		FixRequiredProperties(schema.Else)
	}

	if schema.Items != nil {
		FixRequiredProperties(schema.Items)
	}

	if schema.AdditionalProperties != nil {
		switch v := schema.AdditionalProperties.(type) {
		case *Schema:
			FixRequiredProperties(v)
		case Schema:
			FixRequiredProperties(&v)
			schema.AdditionalProperties = v
		}
	}

	if len(schema.AnyOf) > 0 {
		for _, subSchema := range schema.AnyOf {
			FixRequiredProperties(subSchema)
		}
	}

	if len(schema.AllOf) > 0 {
		for _, subSchema := range schema.AllOf {
			FixRequiredProperties(subSchema)
		}
	}

	if len(schema.OneOf) > 0 {
		for _, subSchema := range schema.OneOf {
			FixRequiredProperties(subSchema)
		}
	}

	if schema.Not != nil {
		FixRequiredProperties(schema.Not)
	}

	// Handle Contains schema
	if schema.Contains != nil {
		FixRequiredProperties(schema.Contains)
	}

	// Handle PropertyNames schema
	if schema.PropertyNames != nil {
		FixRequiredProperties(schema.PropertyNames)
	}

	// Handle AdditionalItems when it's a Schema
	if schema.AdditionalItems != nil {
		switch v := schema.AdditionalItems.(type) {
		case *Schema:
			FixRequiredProperties(v)
		case Schema:
			FixRequiredProperties(&v)
			schema.AdditionalItems = v
		}
	}

	// Handle Definitions
	for _, defSchema := range schema.Definitions {
		FixRequiredProperties(defSchema)
	}

	// Handle PatternProperties
	for _, patternSchema := range schema.PatternProperties {
		FixRequiredProperties(patternSchema)
	}

	return nil
}

// applyRootSchemaProperties copies root-level schema properties from source to target.
// Used for applying @schema.root annotations.
func (s *Schema) applyRootSchemaProperties(source *Schema, valuesPath string) error {
	if source.Title != "" {
		s.Title = source.Title
	}
	if source.Description != "" {
		s.Description = source.Description
	}
	if source.Ref != "" {
		if err := handleSchemaRefs(source, valuesPath); err != nil {
			return err
		}
		s.Ref = source.Ref
	}
	if len(source.Examples) > 0 {
		s.Examples = source.Examples
	}
	if source.Deprecated {
		s.Deprecated = source.Deprecated
	}
	if source.ReadOnly {
		s.ReadOnly = source.ReadOnly
	}
	if source.WriteOnly {
		s.WriteOnly = source.WriteOnly
	}
	if source.AdditionalProperties != nil {
		s.AdditionalProperties = source.AdditionalProperties
	}
	if len(source.Required.Strings) > 0 || source.Required.Bool {
		s.Required = source.Required
	}
	if len(source.PatternProperties) > 0 {
		if s.PatternProperties == nil {
			s.PatternProperties = make(map[string]*Schema)
		}
		for k, v := range source.PatternProperties {
			s.PatternProperties[k] = v
		}
	}
	if len(source.Definitions) > 0 {
		if s.Definition
Download .txt
gitextract_wnyi6ji5/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── build_and_test.yml
│       └── release.yml
├── .gitignore
├── .goreleaser.yaml
├── .pre-commit-config.yaml
├── .pre-commit-hooks.yaml
├── CLAUDE.md
├── Dockerfile
├── LICENSE
├── README.md
├── cliff.toml
├── cmd/
│   └── helm-schema/
│       ├── cli.go
│       ├── main.go
│       ├── main_test.go
│       └── version.go
├── examples/
│   ├── Chart.yaml
│   ├── values.schema.json
│   └── values.yaml
├── go.mod
├── go.sum
├── install-binary.sh
├── pkg/
│   ├── chart/
│   │   ├── chart.go
│   │   ├── chart_test.go
│   │   └── searching/
│   │       └── dependency_charts.go
│   ├── schema/
│   │   ├── annotate.go
│   │   ├── annotate_test.go
│   │   ├── err.go
│   │   ├── err_test.go
│   │   ├── root_schema_test.go
│   │   ├── schema.go
│   │   ├── schema_test.go
│   │   ├── toposort.go
│   │   ├── toposort_test.go
│   │   ├── values_merge.go
│   │   ├── worker.go
│   │   └── worker_test.go
│   └── util/
│       ├── file.go
│       └── file_test.go
├── plugin.yaml
├── renovate.json
├── sign-plugin.sh
├── signing-key.asc
└── tests/
    ├── .gitignore
    ├── charts/
    │   ├── Chart.yaml
    │   ├── ref_input.json
    │   ├── test_annotate_expected.yaml
    │   ├── test_annotate_input.yaml
    │   ├── test_helm_defaults.yaml
    │   ├── test_helm_defaults_expected.schema.json
    │   ├── test_ref.yaml
    │   ├── test_ref_expected.schema.json
    │   ├── test_ref_properties.yaml
    │   ├── test_ref_properties_expected.schema.json
    │   ├── test_ref_toplevel.yaml
    │   ├── test_ref_toplevel_expected.schema.json
    │   ├── test_simple.yaml
    │   └── test_simple_expected.schema.json
    ├── import-values/
    │   ├── child/
    │   │   ├── Chart.yaml
    │   │   └── values.yaml
    │   ├── child-complex/
    │   │   ├── Chart.yaml
    │   │   └── values.yaml
    │   ├── parent/
    │   │   ├── Chart.yaml
    │   │   ├── values.schema.expected.json
    │   │   └── values.yaml
    │   └── parent-complex/
    │       ├── Chart.yaml
    │       ├── values.schema.expected.json
    │       └── values.yaml
    ├── preexisting-schema/
    │   ├── dep-with-schema/
    │   │   ├── Chart.yaml
    │   │   ├── values.schema.json
    │   │   └── values.yaml
    │   └── parent/
    │       ├── Chart.yaml
    │       ├── values.schema.default-expected.json
    │       ├── values.schema.expected.json
    │       └── values.yaml
    ├── run.sh
    └── test-sign-plugin.sh
Download .txt
SYMBOL INDEX (160 symbols across 20 files)

FILE: cmd/helm-schema/cli.go
  function possibleLogLevels (line 13) | func possibleLogLevels() []string {
  function configureLogging (line 23) | func configureLogging() {
  function newCommand (line 35) | func newCommand(run func(cmd *cobra.Command, args []string) error) (*cob...

FILE: cmd/helm-schema/main.go
  function getDependencyNames (line 24) | func getDependencyNames(dependencies []*chart.Dependency, dependenciesFi...
  function mergeSchemaProperties (line 44) | func mergeSchemaProperties(
  function processImportValues (line 91) | func processImportValues(
  function exec (line 178) | func exec(cmd *cobra.Command, _ []string) error {
  function main (line 597) | func main() {

FILE: cmd/helm-schema/main_test.go
  type stringOrArray (line 13) | type stringOrArray
    method UnmarshalJSON (line 15) | func (s *stringOrArray) UnmarshalJSON(value []byte) error {
  type schemaDoc (line 35) | type schemaDoc struct
  type schemaProperty (line 39) | type schemaProperty struct
  function TestExec_ConditionPatchingAndRootConditions (line 44) | func TestExec_ConditionPatchingAndRootConditions(t *testing.T) {
  function TestExec_DependencyFilterSkipsConditionPatching (line 158) | func TestExec_DependencyFilterSkipsConditionPatching(t *testing.T) {
  function TestExec_DependencyAliasConditionPatching (line 257) | func TestExec_DependencyAliasConditionPatching(t *testing.T) {
  function TestExec_KeepExistingDepSchemasPreservesDependencySchema (line 339) | func TestExec_KeepExistingDepSchemasPreservesDependencySchema(t *testing...
  function TestExec_DefaultRegeneratesDependencySchema (line 436) | func TestExec_DefaultRegeneratesDependencySchema(t *testing.T) {
  function TestExec_NoDependenciesSkipsDependencyCharts (line 512) | func TestExec_NoDependenciesSkipsDependencyCharts(t *testing.T) {

FILE: pkg/chart/chart.go
  type Dependency (line 10) | type Dependency struct
  type Maintainer (line 22) | type Maintainer struct
  type ChartFile (line 32) | type ChartFile struct
  function ReadChart (line 71) | func ReadChart(reader io.Reader) (ChartFile, error) {

FILE: pkg/chart/chart_test.go
  function TestReadChartFile (line 10) | func TestReadChartFile(t *testing.T) {
  function TestImportValuesParsing (line 36) | func TestImportValuesParsing(t *testing.T) {
  function TestChartFileParsing (line 125) | func TestChartFileParsing(t *testing.T) {

FILE: pkg/chart/searching/dependency_charts.go
  function extractTGZ (line 15) | func extractTGZ(src, dest string) error {
  function SearchFiles (line 88) | func SearchFiles(chartSearchRoot, startPath, fileName string, dependenci...
  function SearchArchivesOpenTemp (line 130) | func SearchArchivesOpenTemp(startPath string, errs chan<- error) string {

FILE: pkg/schema/annotate.go
  function HasSchemaAnnotation (line 15) | func HasSchemaAnnotation(comment string) bool {
  function typeAnnotationFromTag (line 27) | func typeAnnotationFromTag(tag string) string {
  type InsertionPoint (line 51) | type InsertionPoint struct
  function collectInsertionPoints (line 59) | func collectInsertionPoints(node *yaml.Node) []InsertionPoint {
  function collectInsertionPointsRecursive (line 65) | func collectInsertionPointsRecursive(node *yaml.Node, points *[]Insertio...
  function AnnotateContent (line 108) | func AnnotateContent(content []byte) ([]byte, error) {
  function AnnotateValuesFile (line 165) | func AnnotateValuesFile(valuesPath string, dryRun bool) error {

FILE: pkg/schema/annotate_test.go
  function TestHasSchemaAnnotation (line 10) | func TestHasSchemaAnnotation(t *testing.T) {
  function TestTypeAnnotationFromTag (line 58) | func TestTypeAnnotationFromTag(t *testing.T) {
  function TestCollectInsertionPoints (line 84) | func TestCollectInsertionPoints(t *testing.T) {
  function TestAnnotateContent (line 154) | func TestAnnotateContent(t *testing.T) {

FILE: pkg/schema/err.go
  type CircularError (line 3) | type CircularError struct
    method Error (line 7) | func (e *CircularError) Error() string { return e.msg }

FILE: pkg/schema/err_test.go
  function TestCircularError (line 5) | func TestCircularError(t *testing.T) {

FILE: pkg/schema/root_schema_test.go
  function TestGetRootSchemaFromComment (line 10) | func TestGetRootSchemaFromComment(t *testing.T) {
  function TestYamlToSchemaWithRootAnnotations (line 104) | func TestYamlToSchemaWithRootAnnotations(t *testing.T) {
  function TestRootSchemaDoesNotAffectKeyAnnotations (line 224) | func TestRootSchemaDoesNotAffectKeyAnnotations(t *testing.T) {

FILE: pkg/schema/schema.go
  constant SchemaPrefix (line 26) | SchemaPrefix     = "# @schema"
  constant SchemaRootPrefix (line 27) | SchemaRootPrefix = "# @schema.root"
  constant CommentPrefix (line 28) | CommentPrefix    = "#"
  constant CustomAnnotationPrefix (line 33) | CustomAnnotationPrefix = "x-"
  constant nullTag (line 38) | nullTag      = "!!null"
  constant boolTag (line 39) | boolTag      = "!!bool"
  constant strTag (line 40) | strTag       = "!!str"
  constant intTag (line 41) | intTag       = "!!int"
  constant floatTag (line 42) | floatTag     = "!!float"
  constant timestampTag (line 43) | timestampTag = "!!timestamp"
  constant arrayTag (line 44) | arrayTag     = "!!seq"
  constant mapTag (line 45) | mapTag       = "!!map"
  type SchemaOrBool (line 56) | type SchemaOrBool interface
  type BoolOrArrayOfString (line 60) | type BoolOrArrayOfString struct
    method UnmarshalJSON (line 72) | func (s *BoolOrArrayOfString) UnmarshalJSON(value []byte) error {
    method MarshalJSON (line 84) | func (s *BoolOrArrayOfString) MarshalJSON() ([]byte, error) {
    method UnmarshalYAML (line 93) | func (s *BoolOrArrayOfString) UnmarshalYAML(value *yaml.Node) error {
  function NewBoolOrArrayOfString (line 65) | func NewBoolOrArrayOfString(arr []string, b bool) BoolOrArrayOfString {
  type StringOrArrayOfString (line 118) | type StringOrArrayOfString
    method UnmarshalYAML (line 120) | func (s *StringOrArrayOfString) UnmarshalYAML(value *yaml.Node) error {
    method UnmarshalJSON (line 147) | func (s *StringOrArrayOfString) UnmarshalJSON(value []byte) error {
    method MarshalJSON (line 159) | func (s *StringOrArrayOfString) MarshalJSON() ([]byte, error) {
    method Validate (line 166) | func (s *StringOrArrayOfString) Validate() error {
    method IsEmpty (line 183) | func (s *StringOrArrayOfString) IsEmpty() bool {
    method canDropRequired (line 192) | func (s *StringOrArrayOfString) canDropRequired() bool {
    method Matches (line 200) | func (s *StringOrArrayOfString) Matches(typeString string) bool {
  type Schema (line 250) | type Schema struct
    method MarshalJSON (line 210) | func (s *Schema) MarshalJSON() ([]byte, error) {
    method getJsonKeys (line 318) | func (s Schema) getJsonKeys() []string {
    method UnmarshalYAML (line 333) | func (s *Schema) UnmarshalYAML(node *yaml.Node) error {
    method UnmarshalJSON (line 384) | func (s *Schema) UnmarshalJSON(data []byte) error {
    method HoistDefinitions (line 451) | func (s *Schema) HoistDefinitions() {
    method collectAndHoistDefinitions (line 467) | func (s *Schema) collectAndHoistDefinitions(rootDefs map[string]*Schem...
    method rewriteDefsRefs (line 612) | func (s *Schema) rewriteDefsRefs() {
    method Set (line 685) | func (s *Schema) Set() {
    method DisableRequiredProperties (line 695) | func (s *Schema) DisableRequiredProperties() {
    method GetPropertyAtPath (line 772) | func (s *Schema) GetPropertyAtPath(path string) *Schema {
    method SetPropertyAtPath (line 800) | func (s *Schema) SetPropertyAtPath(path string) *Schema {
    method ToJson (line 829) | func (s Schema) ToJson() ([]byte, error) {
    method Validate (line 871) | func (s Schema) Validate() error {
    method validateSchemaSyntax (line 910) | func (s Schema) validateSchemaSyntax() error {
    method validateTypeConstraints (line 924) | func (s Schema) validateTypeConstraints() error {
    method validateNumericConstraints (line 928) | func (s Schema) validateNumericConstraints() error {
    method validateStringConstraints (line 961) | func (s Schema) validateStringConstraints() error {
    method validateArrayConstraints (line 1015) | func (s Schema) validateArrayConstraints() error {
    method validateObjectConstraints (line 1095) | func (s Schema) validateObjectConstraints() error {
    method validateNestedSchemas (line 1203) | func (s Schema) validateNestedSchemas() error {
    method hasNumericConstraints (line 1243) | func (s Schema) hasNumericConstraints() bool {
    method applyRootSchemaProperties (line 1417) | func (s *Schema) applyRootSchemaProperties(source *Schema, valuesPath ...
  function NewSchema (line 304) | func NewSchema(schemaType string) *Schema {
  constant FormatDateTime (line 839) | FormatDateTime       = "date-time"
  constant FormatTime (line 840) | FormatTime           = "time"
  constant FormatDate (line 841) | FormatDate           = "date"
  constant FormatDuration (line 842) | FormatDuration       = "duration"
  constant FormatEmail (line 843) | FormatEmail          = "email"
  constant FormatIDNEmail (line 844) | FormatIDNEmail       = "idn-email"
  constant FormatHostname (line 845) | FormatHostname       = "hostname"
  constant FormatIDNHostname (line 846) | FormatIDNHostname    = "idn-hostname"
  constant FormatIPv4 (line 847) | FormatIPv4           = "ipv4"
  constant FormatIPv6 (line 848) | FormatIPv6           = "ipv6"
  constant FormatUUID (line 849) | FormatUUID           = "uuid"
  constant FormatURI (line 850) | FormatURI            = "uri"
  constant FormatURIReference (line 851) | FormatURIReference   = "uri-reference"
  constant FormatIRI (line 852) | FormatIRI            = "iri"
  constant FormatIRIReference (line 853) | FormatIRIReference   = "iri-reference"
  constant FormatURITemplate (line 854) | FormatURITemplate    = "uri-template"
  constant FormatJSONPointer (line 855) | FormatJSONPointer    = "json-pointer"
  constant FormatRelJSONPointer (line 856) | FormatRelJSONPointer = "relative-json-pointer"
  constant FormatRegex (line 857) | FormatRegex          = "regex"
  type SkipAutoGenerationConfig (line 1251) | type SkipAutoGenerationConfig struct
  function NewSkipAutoGenerationConfig (line 1255) | func NewSkipAutoGenerationConfig(flag []string) (*SkipAutoGenerationConf...
  function typeFromTag (line 1291) | func typeFromTag(tag string) ([]string, error) {
  function FixRequiredProperties (line 1315) | func FixRequiredProperties(schema *Schema) error {
  function GetRootSchemaFromComment (line 1491) | func GetRootSchemaFromComment(comment string) (Schema, string, error) {
  function GetSchemaFromComment (line 1531) | func GetSchemaFromComment(comment string) (Schema, string, error) {
  function YamlToSchema (line 1578) | func YamlToSchema(
  function helmDocsTypeToSchemaType (line 1873) | func helmDocsTypeToSchemaType(helmDocsType string) (string, error) {
  function castNodeValueByType (line 1904) | func castNodeValueByType(rawValue string, fieldType StringOrArrayOfStrin...
  function decodeNodeValue (line 1937) | func decodeNodeValue(node *yaml.Node) (interface{}, error) {
  function handleSchemaRefs (line 1959) | func handleSchemaRefs(schema *Schema, valuesPath string) error {

FILE: pkg/schema/schema_test.go
  function TestValidate (line 14) | func TestValidate(t *testing.T) {
  function TestUnmarshalYAML (line 217) | func TestUnmarshalYAML(t *testing.T) {
  function TestUnmarshalJSON (line 232) | func TestUnmarshalJSON(t *testing.T) {
  function TestNewDraft7Keywords (line 263) | func TestNewDraft7Keywords(t *testing.T) {
  function TestFloatNumericConstraintsMarshaling (line 550) | func TestFloatNumericConstraintsMarshaling(t *testing.T) {
  function TestNewKeywordsMarshaling (line 603) | func TestNewKeywordsMarshaling(t *testing.T) {
  function TestDisableRequiredPropertiesWithNewFields (line 681) | func TestDisableRequiredPropertiesWithNewFields(t *testing.T) {
  function TestDefsToDefinitionsConversion (line 717) | func TestDefsToDefinitionsConversion(t *testing.T) {
  function TestRefPathRewriting (line 781) | func TestRefPathRewriting(t *testing.T) {
  function TestConstNullMarshaling (line 839) | func TestConstNullMarshaling(t *testing.T) {
  function TestYamlToSchemaConstFromValue (line 903) | func TestYamlToSchemaConstFromValue(t *testing.T) {
  function TestYamlToSchemaPreservesDocumentLocalRootRef (line 997) | func TestYamlToSchemaPreservesDocumentLocalRootRef(t *testing.T) {
  function TestGetPropertyAtPath (line 1043) | func TestGetPropertyAtPath(t *testing.T) {
  function TestSetPropertyAtPath (line 1157) | func TestSetPropertyAtPath(t *testing.T) {
  function TestHoistDefinitions (line 1233) | func TestHoistDefinitions(t *testing.T) {

FILE: pkg/schema/toposort.go
  function TopoSort (line 9) | func TopoSort(results []*Result, allowCircular bool) ([]*Result, error) {

FILE: pkg/schema/toposort_test.go
  function TestTopoSort (line 10) | func TestTopoSort(t *testing.T) {

FILE: pkg/schema/values_merge.go
  function mergeValuesDocuments (line 11) | func mergeValuesDocuments(base *yaml.Node, overlay *yaml.Node) (*yaml.No...
  function mergeValuesNodes (line 39) | func mergeValuesNodes(base *yaml.Node, overlay *yaml.Node) (*yaml.Node, ...
  function mergeMappingNodes (line 65) | func mergeMappingNodes(base *yaml.Node, overlay *yaml.Node) (*yaml.Node,...
  function cloneYAMLNode (line 116) | func cloneYAMLNode(node *yaml.Node) *yaml.Node {
  function mergeCommentText (line 135) | func mergeCommentText(base string, overlay string) string {

FILE: pkg/schema/worker.go
  type Result (line 15) | type Result struct
  function Worker (line 24) | func Worker(

FILE: pkg/schema/worker_test.go
  function TestWorker (line 11) | func TestWorker(t *testing.T) {
  function TestWorker_DryRunDoesNotWriteSchemaReference (line 156) | func TestWorker_DryRunDoesNotWriteSchemaReference(t *testing.T) {
  function TestWorker_MergesMultipleValuesFilesWithRightmostPrecedence (line 196) | func TestWorker_MergesMultipleValuesFilesWithRightmostPrecedence(t *test...

FILE: pkg/util/file.go
  function ReadFileAndFixNewline (line 17) | func ReadFileAndFixNewline(reader io.Reader) ([]byte, error) {
  function appendAndNL (line 25) | func appendAndNL(to, from *[]byte) {
  function appendAndNLStr (line 32) | func appendAndNLStr(to *[]byte, from string) {
  function PrefixFirstYamlDocument (line 38) | func PrefixFirstYamlDocument(line, file string) error {
  function RemoveCommentsFromYaml (line 66) | func RemoveCommentsFromYaml(reader io.Reader) ([]byte, error) {
  function IsRelativeFile (line 167) | func IsRelativeFile(root, relPath string) (string, error) {

FILE: pkg/util/file_test.go
  function TestReadFileAndFixNewline (line 10) | func TestReadFileAndFixNewline(t *testing.T) {
  function TestIsRelativeFile (line 41) | func TestIsRelativeFile(t *testing.T) {
Condensed preview — 77 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (342K chars).
[
  {
    "path": ".editorconfig",
    "chars": 184,
    "preview": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\n\n"
  },
  {
    "path": ".github/workflows/build_and_test.yml",
    "chars": 2038,
    "preview": "---\nname: build and test\n\non: push\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n   "
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 4561,
    "preview": "---\nname: release\n\non:\n  push:\n    tags:\n      - \"*\"\n\npermissions:\n  packages: write\n  contents: write\n\njobs:\n  goreleas"
  },
  {
    "path": ".gitignore",
    "chars": 100,
    "preview": "./values.yaml\n./values.schema.json\n\ndist/\nRELEASE_NOTES.md\n.vscode\n\n/helm-schema\n/tests/helm-schema\n"
  },
  {
    "path": ".goreleaser.yaml",
    "chars": 2328,
    "preview": "---\n# yaml-language-server: $schema=https://goreleaser.com/static/schema.json\n# vim: set ts=2 sw=2 tw=0 fo=cnqoj\n\nversio"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 624,
    "preview": "---\nrepos:\n  - repo: https://github.com/dadav/helm-schema\n    rev: 0.22.0\n    hooks:\n      - id: helm-schema\n        # f"
  },
  {
    "path": ".pre-commit-hooks.yaml",
    "chars": 784,
    "preview": "---\n- id: helm-schema\n  description: Uses helm-schema to create a jsonschema.\n  entry: helm-schema\n  files: (Chart|value"
  },
  {
    "path": "CLAUDE.md",
    "chars": 13014,
    "preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
  },
  {
    "path": "Dockerfile",
    "chars": 361,
    "preview": "FROM alpine:3.23@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11\nARG TARGETPLATFORM\nRUN adduser "
  },
  {
    "path": "LICENSE",
    "chars": 1062,
    "preview": "MIT License\n\nCopyright (c) 2023 dadav\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
  },
  {
    "path": "README.md",
    "chars": 34836,
    "preview": "# helm-schema\n\n<p align=\"center\">\n  <img src=\"images/logo.png\" width=\"400\" />\n  <br />\n  <a href=\"https://github.com/dad"
  },
  {
    "path": "cliff.toml",
    "chars": 1162,
    "preview": "[changelog]\nheader = \"\"\nbody = \"\"\"\n{% if version %}## {{ version }} - {{ timestamp | date(format=\"%Y-%m-%d\") }}\n{% else "
  },
  {
    "path": "cmd/helm-schema/cli.go",
    "chars": 3900,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com"
  },
  {
    "path": "cmd/helm-schema/main.go",
    "chars": 18021,
    "preview": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n"
  },
  {
    "path": "cmd/helm-schema/main_test.go",
    "chars": 15602,
    "preview": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/spf13/viper\"\n\t\"github.com/stretc"
  },
  {
    "path": "cmd/helm-schema/version.go",
    "chars": 44,
    "preview": "package main\n\nvar version string = \"0.23.2\"\n"
  },
  {
    "path": "examples/Chart.yaml",
    "chars": 1143,
    "preview": "apiVersion: v2\nname: example\ndescription: A Helm chart for Kubernetes\n\n# A chart can be either an 'application' or a 'li"
  },
  {
    "path": "examples/values.schema.json",
    "chars": 4788,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"global"
  },
  {
    "path": "examples/values.yaml",
    "chars": 2623,
    "preview": "# vim: set ft=yaml:\n# yaml-language-server: $schema=values.schema.json\n\n# This is an example values.yaml file, it aims t"
  },
  {
    "path": "go.mod",
    "chars": 1778,
    "preview": "module github.com/dadav/helm-schema\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/dadav/go-jsonpointer v0.0.0-20240918181927-335cbee"
  },
  {
    "path": "go.sum",
    "chars": 7797,
    "preview": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMI"
  },
  {
    "path": "install-binary.sh",
    "chars": 5403,
    "preview": "#!/usr/bin/env sh\n\n# Shamelessly copied from https://github.com/technosophos/helm-template/blob/master/install-binary.sh"
  },
  {
    "path": "pkg/chart/chart.go",
    "chars": 3118,
    "preview": "package chart\n\nimport (\n\t\"io\"\n\n\t\"github.com/dadav/helm-schema/pkg/util\"\n\tyaml \"gopkg.in/yaml.v3\"\n)\n\ntype Dependency stru"
  },
  {
    "path": "pkg/chart/chart_test.go",
    "chars": 4875,
    "preview": "package chart\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestReadChartFile(t *testing.T) {\n\tdata := []by"
  },
  {
    "path": "pkg/chart/searching/dependency_charts.go",
    "chars": 3764,
    "preview": "package searching\n\nimport (\n\t\"archive/tar\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"github.com/dadav/helm-schema/pkg/chart\"\n\t\"gopkg.in/"
  },
  {
    "path": "pkg/schema/annotate.go",
    "chars": 5193,
    "preview": "package schema\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// Ha"
  },
  {
    "path": "pkg/schema/annotate_test.go",
    "chars": 6139,
    "preview": "package schema\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestHasSchemaAnnotation(t *testing.T) {\n\ttes"
  },
  {
    "path": "pkg/schema/err.go",
    "chars": 115,
    "preview": "package schema\n\ntype CircularError struct {\n\tmsg string\n}\n\nfunc (e *CircularError) Error() string { return e.msg }\n"
  },
  {
    "path": "pkg/schema/err_test.go",
    "chars": 587,
    "preview": "package schema\n\nimport \"testing\"\n\nfunc TestCircularError(t *testing.T) {\n\ttests := []struct {\n\t\tname    string\n\t\tmessage"
  },
  {
    "path": "pkg/schema/root_schema_test.go",
    "chars": 7184,
    "preview": "package schema\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestGetRootSchemaFromComment(t *testing.T) {"
  },
  {
    "path": "pkg/schema/schema.go",
    "chars": 61970,
    "preview": "package schema\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strconv"
  },
  {
    "path": "pkg/schema/schema_test.go",
    "chars": 28306,
    "preview": "package schema\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/magiconair/properties/as"
  },
  {
    "path": "pkg/schema/toposort.go",
    "chars": 2126,
    "preview": "package schema\n\nimport (\n\t\"fmt\"\n)\n\n// TopoSort uses topological sorting to sort the results\n// If allowCircular is true,"
  },
  {
    "path": "pkg/schema/toposort_test.go",
    "chars": 3249,
    "preview": "package schema\n\nimport (\n\t\"testing\"\n\n\t\"github.com/dadav/helm-schema/pkg/chart\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n"
  },
  {
    "path": "pkg/schema/values_merge.go",
    "chars": 4224,
    "preview": "package schema\n\nimport (\n\t\"fmt\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// mergeValuesDocuments merges YAML documents using Helm-style p"
  },
  {
    "path": "pkg/schema/worker.go",
    "chars": 4203,
    "preview": "package schema\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/dadav/helm-schema/pkg/chart\"\n\t\""
  },
  {
    "path": "pkg/schema/worker_test.go",
    "chars": 6774,
    "preview": "package schema\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWorker(t *"
  },
  {
    "path": "pkg/util/file.go",
    "chars": 4769,
    "preview": "package util\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"gopkg.in/yaml.v3\"\n"
  },
  {
    "path": "pkg/util/file_test.go",
    "chars": 2508,
    "preview": "package util\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestReadFileAndFixNewline(t *testing.T) {\n\ttes"
  },
  {
    "path": "plugin.yaml",
    "chars": 294,
    "preview": "---\nname: \"schema\"\nversion: \"0.23.2\"\nusage: \"generate jsonschemas for your helm charts\"\ndescription: \"generate jsonschem"
  },
  {
    "path": "renovate.json",
    "chars": 182,
    "preview": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\"config:best-practices\", \"schedule:ear"
  },
  {
    "path": "sign-plugin.sh",
    "chars": 3918,
    "preview": "#!/bin/bash\n# Script to sign Helm plugin tarballs for Helm v4 verification\n# This creates .prov (provenance) files using"
  },
  {
    "path": "signing-key.asc",
    "chars": 652,
    "preview": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmDMEaRt56xYJKwYBBAHaRw8BAQdA4RAN5LipEazZbSMV+BLcJh1BHY39WxIxS8tm\nalFiioi0HGRhZGF2I"
  },
  {
    "path": "tests/.gitignore",
    "chars": 98,
    "preview": "*_generated.schema.json\nhelm-schema\ntest_repo_example.yaml\ntest_repo_example_expected.schema.json\n"
  },
  {
    "path": "tests/charts/Chart.yaml",
    "chars": 99,
    "preview": "apiVersion: v2\nname: test\ndescription: Test chart for helm-schema\ntype: application\nversion: 0.1.0\n"
  },
  {
    "path": "tests/charts/ref_input.json",
    "chars": 296,
    "preview": "{\n  \"foo\": {\n    \"type\": \"string\",\n    \"description\": \"from ref\"\n  },\n  \"bar\": {\n    \"type\": \"object\",\n    \"description\""
  },
  {
    "path": "tests/charts/test_annotate_expected.yaml",
    "chars": 807,
    "preview": "# @schema\n# type: integer\n# @schema\nreplicaCount: 1\n\n# @schema\n# type: object\n# @schema\nimage:\n  # @schema\n  # type: str"
  },
  {
    "path": "tests/charts/test_annotate_input.yaml",
    "chars": 320,
    "preview": "replicaCount: 1\n\nimage:\n  repository: nginx\n  pullPolicy: IfNotPresent\n  # Overrides the image tag whose default is the "
  },
  {
    "path": "tests/charts/test_helm_defaults.yaml",
    "chars": 2356,
    "preview": "# Default values for foo.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplic"
  },
  {
    "path": "tests/charts/test_helm_defaults_expected.schema.json",
    "chars": 9863,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"affini"
  },
  {
    "path": "tests/charts/test_ref.yaml",
    "chars": 155,
    "preview": "---\n# @schema\n# $ref: ref_input.json#/foo\n# @schema\n# Not from ref\nfoo: bar\n# @schema\n# $ref: ref_input.json#/bar\n# @sch"
  },
  {
    "path": "tests/charts/test_ref_expected.schema.json",
    "chars": 799,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"bar\": "
  },
  {
    "path": "tests/charts/test_ref_properties.yaml",
    "chars": 253,
    "preview": "# @schema\n# definitions:\n#   port:\n#     type: integer\n#     minimum: 1\n#     maximum: 65535\n# properties:\n#   httpPort:"
  },
  {
    "path": "tests/charts/test_ref_properties_expected.schema.json",
    "chars": 814,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"definitions\": {\n    \"port\""
  },
  {
    "path": "tests/charts/test_ref_toplevel.yaml",
    "chars": 124,
    "preview": "# @schema\n# definitions:\n#   toplevel:\n#     description: \"Top Level\"\n# $ref: \"#/definitions/toplevel\"\n# @schema\ntopleve"
  },
  {
    "path": "tests/charts/test_ref_toplevel_expected.schema.json",
    "chars": 513,
    "preview": "{\n\t\"$schema\": \"http://json-schema.org/draft-07/schema#\",\n\t\"additionalProperties\": false,\n\t\"definitions\": {\n\t\t\"toplevel\":"
  },
  {
    "path": "tests/charts/test_simple.yaml",
    "chars": 52,
    "preview": "---\n# @schema\n# description: foo\n# @schema\nfoo: bar\n"
  },
  {
    "path": "tests/charts/test_simple_expected.schema.json",
    "chars": 482,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"foo\": "
  },
  {
    "path": "tests/import-values/child/Chart.yaml",
    "chars": 82,
    "preview": "apiVersion: v2\nname: child\nversion: 1.0.0\ndescription: A child chart with exports\n"
  },
  {
    "path": "tests/import-values/child/values.yaml",
    "chars": 555,
    "preview": "# @schema\n# type: object\n# @schema\nexports:\n  # @schema\n  # type: object\n  # @schema\n  defaults:\n    # @schema\n    # typ"
  },
  {
    "path": "tests/import-values/child-complex/Chart.yaml",
    "chars": 120,
    "preview": "apiVersion: v2\nname: child-complex\nversion: 1.0.0\ndescription: A child chart with nested data for complex import-values\n"
  },
  {
    "path": "tests/import-values/child-complex/values.yaml",
    "chars": 347,
    "preview": "# @schema\n# type: object\n# @schema\ndata:\n  # @schema\n  # type: object\n  # @schema\n  database:\n    # @schema\n    # type: "
  },
  {
    "path": "tests/import-values/parent/Chart.yaml",
    "chars": 208,
    "preview": "apiVersion: v2\nname: parent\nversion: 1.0.0\ndescription: A parent chart using import-values\ndependencies:\n  - name: child"
  },
  {
    "path": "tests/import-values/parent/values.schema.expected.json",
    "chars": 1000,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"appNam"
  },
  {
    "path": "tests/import-values/parent/values.yaml",
    "chars": 209,
    "preview": "# @schema\n# type: string\n# description: Application name\n# @schema\nappName: myapp\n\n# No @schema annotation - inferred sc"
  },
  {
    "path": "tests/import-values/parent-complex/Chart.yaml",
    "chars": 276,
    "preview": "apiVersion: v2\nname: parent-complex\nversion: 1.0.0\ndescription: A parent chart using complex import-values form\ndependen"
  },
  {
    "path": "tests/import-values/parent-complex/values.schema.expected.json",
    "chars": 1032,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"db\": {"
  },
  {
    "path": "tests/import-values/parent-complex/values.yaml",
    "chars": 98,
    "preview": "# @schema\n# type: string\n# description: Application environment\n# @schema\nenvironment: production\n"
  },
  {
    "path": "tests/preexisting-schema/dep-with-schema/Chart.yaml",
    "chars": 104,
    "preview": "apiVersion: v2\nname: dep-with-schema\nversion: 1.0.0\ndescription: A dependency chart with its own schema\n"
  },
  {
    "path": "tests/preexisting-schema/dep-with-schema/values.schema.json",
    "chars": 444,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"port\": {\n      \"typ"
  },
  {
    "path": "tests/preexisting-schema/dep-with-schema/values.yaml",
    "chars": 27,
    "preview": "port: 8080\nhost: localhost\n"
  },
  {
    "path": "tests/preexisting-schema/parent/Chart.yaml",
    "chars": 201,
    "preview": "apiVersion: v2\nname: parent\nversion: 1.0.0\ndescription: A parent chart depending on dep-with-schema\ndependencies:\n  - na"
  },
  {
    "path": "tests/preexisting-schema/parent/values.schema.default-expected.json",
    "chars": 1156,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"appNam"
  },
  {
    "path": "tests/preexisting-schema/parent/values.schema.expected.json",
    "chars": 1035,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"appNam"
  },
  {
    "path": "tests/preexisting-schema/parent/values.yaml",
    "chars": 15,
    "preview": "appName: myapp\n"
  },
  {
    "path": "tests/run.sh",
    "chars": 4066,
    "preview": "#!/usr/bin/env bash\n\nrc=0\n\ncd charts\n\ncp ../../examples/values.yaml test_repo_example.yaml\ncp ../../examples/values.sche"
  },
  {
    "path": "tests/test-sign-plugin.sh",
    "chars": 3092,
    "preview": "#!/bin/bash\n# Test script for sign-plugin.sh\n# Creates an isolated GPG environment, generates a test key, and validates "
  }
]

About this extraction

This page contains the full source code of the dadav/helm-schema GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 77 files (300.2 KB), approximately 86.3k tokens, and a symbol index with 160 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!