Repository: python-jsonschema/check-jsonschema

Issue:
Include Buildkite pipeline schema
As a user of Buildkite, I would super appreciate it if their pipeline schema was bundled with check-jsonschema :pray: 

https://github.com/buildkite/pipeline-schema/

Discussion:

Patch:
diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml
index 329071f..1cc24ad 100644
--- a/.pre-commit-hooks.yaml
+++ b/.pre-commit-hooks.yaml
@@ -34,6 +34,20 @@
   files: ^bamboo-specs/.*\.(yml|yaml)$
   types: [yaml]
 
+- id: check-buildkite
+  name: Validate Buildkite Pipelines
+  description: 'Validate Buildkite Pipelines against the schema provided by Buildkite'
+  entry: check-jsonschema --builtin-schema vendor.buildkite
+  language: python
+  files: >
+    (?x)^(
+      buildkite\.(yml|yaml|json)|
+      buildkite\.(.+)\.(yml|yaml|json)|
+      (.*/)?\.buildkite/pipeline\.(yml|yaml|json)|
+      (.*/)?\.buildkite/pipeline\.(.+)\.(yml|yaml|json)
+    )$
+  types_or: [json,yaml]
+
 - id: check-dependabot
   name: Validate Dependabot Config (v2)
   description: 'Validate Dependabot Config (v2) against the schema provided by SchemaStore'
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 210fa02..a9900d9 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -14,6 +14,7 @@ Unreleased
 - Add ``--fill-defaults`` argument which eagerly populates ``"default"``
   values whenever they are encountered and a value is not already present
   (:issue:`200`)
+- Add Buildkite schema and pre-commit hook (:issue:`198`)
 
 0.19.2
 ------
diff --git a/docs/precommit_usage.rst b/docs/precommit_usage.rst
index 0f1d790..e5bb80c 100644
--- a/docs/precommit_usage.rst
+++ b/docs/precommit_usage.rst
@@ -71,6 +71,20 @@ Validate Bamboo Specs against the schema provided by SchemaStore
         - id: check-bamboo-spec
 
 
+``check-buildkite``
+~~~~~~~~~~~~~~~~~~~
+
+Validate Buildkite Pipelines against the schema provided by Buildkite
+
+.. code-block:: yaml
+    :caption: example config
+
+    - repo: https://github.com/python-jsonschema/check-jsonschema
+      rev: 0.19.2
+      hooks:
+        - id: check-buildkite
+
+
 ``check-dependabot``
 ~~~~~~~~~~~~~~~~~~~~
 
diff --git a/docs/usage.rst b/docs/usage.rst
index d114ced..9ccbb64 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -70,6 +70,7 @@ SchemaStore and other sources:
 
 - ``vendor.azure-pipelines``
 - ``vendor.bamboo-spec``
+- ``vendor.buildkite``
 - ``vendor.dependabot``
 - ``vendor.github-actions``
 - ``vendor.github-workflows``
diff --git a/scripts/generate-hooks-config.py b/scripts/generate-hooks-config.py
index 7b0034c..4e7c5ed 100644
--- a/scripts/generate-hooks-config.py
+++ b/scripts/generate-hooks-config.py
@@ -2,6 +2,7 @@
 from __future__ import annotations
 
 import importlib.metadata
+import typing as t
 
 from check_jsonschema.catalog import SCHEMA_CATALOG
 
@@ -40,10 +41,21 @@ def update_hook_config(new_config: str) -> None:
         fp.write(content)
 
 
-def format_hook(config) -> str:
+def generate_hook_lines(config) -> t.Iterator[str]:
+    yield f"- id: {config['id']}"
+    yield f"  name: {config['name']}"
+    yield f"  description: '{config['description']}'"
+
     add_args = " ".join(config.get("add_args", []))
     if add_args:
         add_args = " " + add_args
+    yield (
+        "  entry: check-jsonschema --builtin-schema "
+        f"vendor.{config['schema_name']}{add_args}"
+    )
+
+    yield "  language: python"
+
     if isinstance(config["files"], list):
         config[
             "files"
@@ -54,23 +66,23 @@ def format_hook(config) -> str:
             "|\n      ".join(config["files"])
         )
 
-    config_str = f"""\
-- id: {config["id"]}
-  name: {config["name"]}
-  description: '{config["description"]}'
-  entry: check-jsonschema --builtin-schema vendor.{config["schema_name"]}{add_args}
-  language: python
-  files: {config["files"]}
-"""
+    yield f"  files: {config['files']}"
+
     if "types" in config:
-        config_str += f"""\
-  types: [{','.join(config['types'])}]
-"""
-    return config_str
+        yield f"  types: [{','.join(config['types'])}]"
+    if "types_or" in config:
+        yield f"  types_or: [{','.join(config['types_or'])}]"
+
+    yield ""
+
+
+def generate_all_hook_config_lines() -> t.Iterator[str]:
+    for hook_config in iter_catalog_hooks():
+        yield from generate_hook_lines(hook_config)
 
 
-def generate_hook_config() -> str:
-    return "\n".join(format_hook(h) for h in iter_catalog_hooks())
+def format_all_hook_config() -> str:
+    return "\n".join(generate_all_hook_config_lines())
 
 
 def update_usage_list_schemas() -> None:
@@ -139,7 +151,7 @@ def update_docs() -> None:
 
 
 def main() -> None:
-    update_hook_config(generate_hook_config())
+    update_hook_config(format_all_hook_config())
     update_docs()
 
 
diff --git a/scripts/vendor-schemas.py b/scripts/vendor-schemas.py
index a064524..c12db59 100644
--- a/scripts/vendor-schemas.py
+++ b/scripts/vendor-schemas.py
@@ -50,6 +50,11 @@ def load_old_hashes() -> None:
                 OLD_HASHES[name] = fp.read().strip()
 
 
+def normalize_schema_contents(data: bytes) -> bytes:
+    lines = data.split(b"\n")
+    return b"\n".join(line.rstrip(b" ") for line in lines)
+
+
 def download_schemas() -> None:
     print("downloading schemas to check for updates")
     session = requests.Session()
@@ -59,17 +64,18 @@ def download_schemas() -> None:
 
         print(f"  {schema_name} ({schema_url})")
         res = session.get(schema_url)
+        new_content = normalize_schema_contents(res.content)
+
         sha = hashlib.sha256()
-        sha.update(res.content)
+        sha.update(new_content)
         new_digest = sha.hexdigest()
 
         # early abort if downloaded content matches old hash
         if new_digest == OLD_HASHES.get(schema_name):
             continue
 
-        print("    content changed")
         with open(schema2filename(schema_name), "wb") as fp:
-            fp.write(res.content)
+            fp.write(new_content)
         UPDATED_SCHEMAS.add(schema_name)
 
 
@@ -105,7 +111,7 @@ def save_new_hashes() -> None:
             _save_new_hash(name, digest)
 
         # if the existing hash does not match the new hash, save the new one
-        if digest != OLD_HASHES[name]:
+        elif digest != OLD_HASHES[name]:
             _save_new_hash(name, digest)
 
 
diff --git a/src/check_jsonschema/builtin_schemas/vendor/LICENSE.buildkite b/src/check_jsonschema/builtin_schemas/vendor/LICENSE.buildkite
new file mode 100644
index 0000000..22be480
--- /dev/null
+++ b/src/check_jsonschema/builtin_schemas/vendor/LICENSE.buildkite
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Buildkite
+
+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.
diff --git a/src/check_jsonschema/builtin_schemas/vendor/README.md b/src/check_jsonschema/builtin_schemas/vendor/README.md
index 57a8ebb..8258e3c 100644
--- a/src/check_jsonschema/builtin_schemas/vendor/README.md
+++ b/src/check_jsonschema/builtin_schemas/vendor/README.md
@@ -29,3 +29,8 @@ renovatebot
 The GitLab CI schema is provided by GitLab and licensed under the license for
 the public gitlab repo. In particular, it falls under the "MIT Expat" portion
 of the license for that repo.
+
+### Buildkite
+
+The Buildkite schema is provided by Buildkite and licensed under the license
+for their 'pipeline-schema' repo.
diff --git a/src/check_jsonschema/builtin_schemas/vendor/buildkite.json b/src/check_jsonschema/builtin_schemas/vendor/buildkite.json
new file mode 100644
index 0000000..3dbeadb
--- /dev/null
+++ b/src/check_jsonschema/builtin_schemas/vendor/buildkite.json
@@ -0,0 +1,1259 @@
+{
+  "title": "JSON schema for Buildkite pipeline configuration files",
+  "$schema": "http://json-schema.org/draft-06/schema",
+  "fileMatch": [
+    "buildkite.yml",
+    "buildkite.yaml",
+    "buildkite.json",
+    "buildkite.*.yml",
+    "buildkite.*.yaml",
+    "buildkite.*.json",
+    ".buildkite/pipeline.yml",
+    ".buildkite/pipeline.yaml",
+    ".buildkite/pipeline.json",
+    ".buildkite/pipeline.*.yml",
+    ".buildkite/pipeline.*.yaml",
+    ".buildkite/pipeline.*.json"
+  ],
+  "definitions": {
+    "commonOptions": {
+      "allowDependencyFailure": {
+        "type": "boolean",
+        "description": "Whether to proceed with this step and further steps if a step named in the depends_on attribute fails",
+        "default": false
+      },
+      "agents": {
+        "oneOf": [
+          { "$ref": "#/definitions/commonOptions/agentsObject" },
+          { "$ref": "#/definitions/commonOptions/agentsList" }
+        ]
+      },
+      "agentsObject": {
+        "type": "object",
+        "description": "Query rules to target specific agents",
+        "examples": [
+          { "queue": "deploy" },
+          { "ruby": "2*" }
+        ]
+      },
+      "agentsList": {
+        "type": "array",
+        "description": "Query rules to target specific agents in k=v format",
+        "examples": [
+          "queue=default",
+          "xcode=true"
+        ],
+        "items": {
+          "type": "string"
+        }
+      },
+      "automaticRetry": {
+        "type": "object",
+        "properties": {
+          "exit_status": {
+            "description": "The exit status number that will cause this job to retry",
+            "anyOf": [
+              {
+                "type": "string",
+                "enum": [ "*" ]
+              },
+              {
+                "type": "number"
+              }
+            ]
+          },
+          "limit": {
+            "type": "integer",
+            "description": "The number of times this job can be retried",
+            "minimum": 1,
+            "maximum": 10
+          }
+        },
+        "additionalProperties": false
+      },
+      "branches": {
+        "description": "Which branches will include this step in their builds",
+        "anyOf": [
+          {
+            "type": "string"
+          },
+          {
+            "type": "array",
+            "items": { "type": "string" }
+          }
+        ],
+        "examples": [
+          "master",
+          [ "feature/*", "chore/*" ]
+        ]
+      },
+      "dependsOn": {
+        "description": "The step keys for a step to depend on",
+        "anyOf": [
+          {"type": "null"},
+          {"type": "string"},
+          {
+            "type": "array",
+            "items": {
+              "anyOf": [
+                {"type": "string"},
+                {
+                  "type": "object",
+                  "properties": {
+                    "step": { "type": "string" },
+                    "allow_failure": {
+                      "type": "boolean",
+                      "default": false
+                    }
+                  },
+                  "additionalProperties": false
+                }
+              ]
+            }
+          }
+        ]
+      },
+      "env": {
+        "type": "object",
+        "description": "Environment variables for this step",
+        "examples": [
+          { "NODE_ENV": "test" }
+        ]
+      },
+      "identifier": {
+        "type": "string",
+        "description": "A string identifier",
+        "examples": [ "an-id" ]
+      },
+      "if": {
+        "type": "string",
+        "description": "A boolean expression that omits the step when false",
+        "examples": [ "build.message != 'skip me'", "build.branch == 'master'" ]
+      },
+      "key": {
+        "type": "string",
+        "description": "A unique identifier for a step, must not resemble a UUID",
+        "examples": [ "deploy-staging", "test-integration" ]
+      },
+      "label": {
+        "type": "string",
+        "description": "The label that will be displayed in the pipeline visualisation in Buildkite. Supports emoji.",
+        "examples": [ ":docker: Build" ]
+      },
+      "buildNotify": {
+        "type": "array",
+        "description": "Array of notification options for this step",
+        "items": {
+          "oneOf": [
+            {
+              "type": "string",
+              "enum": ["github_check", "github_commit_status"]
+            },
+            {
+              "type": "object",
+              "properties": {
+                "email": {
+                  "type": "string"
+                },
+                "if": {
+                  "$ref": "#/definitions/commonOptions/if"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "type": "object",
+              "properties": {
+                "basecamp_campfire": {
+                  "type": "string"
+                },
+                "if": {
+                  "$ref": "#/definitions/commonOptions/if"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "type": "object",
+              "properties": {
+                "slack": {
+                  "oneOf": [
+                    {
+                      "type": "string"
+                    },
+                    {
+                      "type": "object",
+                      "properties": {
+                        "channels": {
+                          "type": "array",
+                          "items": {
+                            "type": "string"
+                          }
+                        },
+                        "message": {
+                          "type": "string"
+                        }
+                      }
+                    }
+                  ]
+                },
+                "if": {
+                  "$ref": "#/definitions/commonOptions/if"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "type": "object",
+              "properties": {
+                "webhook": {
+                  "type": "string"
+                },
+                "if": {
+                  "$ref": "#/definitions/commonOptions/if"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "type": "object",
+              "properties": {
+                "pagerduty_change_event": {
+                  "type": "string"
+                },
+                "if": {
+                  "$ref": "#/definitions/commonOptions/if"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "type": "object",
+              "properties": {
+                "github_commit_status": {
+                  "type": "object",
+                  "properties": {
+                    "context": {
+                      "description": "GitHub commit status name",
+                      "type": "string"
+                    }
+                  }
+                },
+                "if": {
+                  "$ref": "#/definitions/commonOptions/if"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "type": "object",
+              "properties": {
+                "github_check": {
+                  "type": "object",
+                  "properties": {
+                    "context": {
+                      "description": "GitHub commit status name",
+                      "type": "string"
+                    }
+                  }
+                },
+                "if": {
+                  "$ref": "#/definitions/commonOptions/if"
+                }
+              },
+              "additionalProperties": false
+            }
+          ]
+        }
+      },
+      "fields": {
+        "type": "array",
+        "description": "A list of input fields required to be filled out before unblocking the step",
+        "items": {
+          "oneOf": [
+            {
+              "type": "object",
+              "properties": {
+                "text": {
+                  "type": "string",
+                  "description": "The text input name",
+                  "examples": [
+                    "Release Name"
+                  ]
+                },
+                "key": {
+                  "type": "string",
+                  "description": "The meta-data key that stores the field's input",
+                  "pattern": "^[a-zA-Z0-9-]+$",
+                  "examples": [
+                    "release-name"
+                  ]
+                },
+                "hint": {
+                  "type": "string",
+                  "description": "The explanatory text that is shown after the label",
+                  "examples": [
+                    "What’s the code name for this release? :name_badge:"
+                  ]
+                },
+                "required": {
+                  "type": "boolean",
+                  "default": true,
+                  "description": "Whether the field is required for form submission"
+                },
+                "default": {
+                  "type": "string",
+                  "description": "The value that is pre-filled in the text field",
+                  "examples": [
+                    "Flying Dolphin"
+                  ]
+                }
+              },
+              "additionalProperties": false,
+              "required": [
+                "key"
+              ]
+            },
+            {
+              "type": "object",
+              "properties": {
+                "select": {
+                  "type": "string",
+                  "description": "The text input name",
+                  "examples": [
+                    "Release Stream"
+                  ]
+                },
+                "key": {
+                  "type": "string",
+                  "description": "The meta-data key that stores the field's input",
+                  "pattern": "^[a-zA-Z0-9-]+$",
+                  "examples": [
+                    "release-stream"
+                  ]
+                },
+                "default": {
+                  "oneOf": [
+                    {
+                      "type": "string"
+                    },
+                    {
+                      "type": "array",
+                      "items": { "type": "string" }
+                    }
+                  ],
+                  "description": "The value of the option(s) that will be pre-selected in the dropdown",
+                  "examples": [ "beta" , [ "alpha" , "beta" ] ]
+                },
+                "hint": {
+                  "type": "string",
+                  "description": "The explanatory text that is shown after the label",
+                  "examples": [
+                    "What’s the code name for this release? :name_badge:"
+                  ]
+                },
+                "multiple": {
+                  "type": "boolean",
+                  "description": "Whether more than one option may be selected",
+                  "default": false
+                  },
+                "options": {
+                  "type": "array",
+                  "items": {
+                    "type": "object",
+                    "properties": {
+                      "label": {
+                        "type": "string",
+                        "description": "The text displayed on the select list item",
+                        "examples": [ "Stable" ]
+                      },
+                      "value": {
+                        "type": "string",
+                        "description": "The value to be stored as meta-data",
+                        "examples": [ "stable" ]
+                      },
+                      "hint": {
+                        "type": "string",
+                        "description": "The text displayed directly under the select field’s label",
+                        "examples": [
+                          "Which release stream does this belong in? :fork:"
+                        ]
+                      },
+                      "required": {
+                        "type": "boolean",
+                        "default": true,
+                        "description": "Whether the field is required for form submission"
+                      }
+                    },
+                    "additionalProperties": false,
+                    "required": [
+                      "label",
+                      "value"
+                    ]
+                  }
+                },
+                "required": {
+                  "type": "boolean",
+                  "default": true,
+                  "description": "Whether the field is required for form submission"
+                }
+              },
+              "additionalProperties": false,
+              "required": [
+                "key",
+                "options"
+              ]
+            }
+          ]
+        }
+      },
+      "matrixElement": {
+        "oneOf": [
+          {"type": "string"},
+          {"type": "integer"},
+          {"type": "boolean"}
+        ]
+      },
+      "prompt": {
+        "type": "string",
+        "description": "The instructional message displayed in the dialog box when the unblock step is activated",
+        "examples": [
+          "Release to production?"
+        ]
+      },
+      "skip": {
+        "anyOf": [
+          { "type": "boolean" },
+          { "type": "string" }
+        ],
+        "description": "Whether this step should be skipped. You can specify a reason for using a string.",
+        "examples": [
+          true,
+          false,
+          "My reason"
+        ]
+      },
+      "softFail": {
+        "description": "The conditions for marking the step as a soft-fail.",
+        "anyOf": [
+          {
+            "type": "boolean"
+          },
+          {
+            "type": "array",
+            "items": {
+              "anyOf": [
+                {
+                  "type": "object",
+                  "properties": {
+                    "exit_status": {
+                      "description": "The exit status number that will cause this job to soft-fail",
+                      "anyOf": [
+                        {
+                          "type": "string",
+                          "enum": [ "*" ]
+                        },
+                        {
+                          "type": "number"
+                        }
+                      ]
+                    }
+                  }
+                }
+              ]
+            }
+          }
+        ]
+      }
+    },
+    "blockStep": {
+      "type": "object",
+      "properties": {
+        "allow_dependency_failure": {
+          "$ref": "#/definitions/commonOptions/allowDependencyFailure"
+        },
+        "block": {
+          "type": "string",
+          "description": "The label of the block step"
+        },
+        "blocked_state": {
+          "type": "string",
+          "description": "The state that the build is set to when the build is blocked by this block step",
+          "enum": [ "passed", "failed", "running" ]
+        },
+        "branches": {
+          "$ref": "#/definitions/commonOptions/branches"
+        },
+        "depends_on": {
+          "$ref": "#/definitions/commonOptions/dependsOn"
+        },
+        "fields": {
+          "$ref": "#/definitions/commonOptions/fields"
+        },
+        "id": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "identifier": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "if": {
+          "$ref": "#/definitions/commonOptions/if"
+        },
+        "key": {
+          "$ref": "#/definitions/commonOptions/key"
+        },
+        "label": {
+          "$ref": "#/definitions/commonOptions/label"
+        },
+        "name": {
+          "$ref": "#/definitions/commonOptions/label"
+        },
+        "prompt": {
+          "$ref": "#/definitions/commonOptions/prompt"
+        },
+        "type": {
+          "type": "string",
+          "enum": [ "block" ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "nestedBlockStep": {
+      "type": "object",
+      "properties": {
+        "block": {
+          "$ref": "#/definitions/blockStep"
+        }
+      },
+      "additionalProperties": false
+    },
+    "stringBlockStep": {
+      "type": "string",
+      "description": "Pauses the execution of a build and waits on a user to unblock it",
+      "enum": [ "block" ]
+    },
+    "inputStep": {
+      "type": "object",
+      "properties": {
+        "allow_dependency_failure": {
+          "$ref": "#/definitions/commonOptions/allowDependencyFailure"
+        },
+        "input": {
+          "type": "string",
+          "description": "The label of the input step"
+        },
+        "branches": {
+          "$ref": "#/definitions/commonOptions/branches"
+        },
+        "depends_on": {
+          "$ref": "#/definitions/commonOptions/dependsOn"
+        },
+        "fields": {
+          "$ref": "#/definitions/commonOptions/fields"
+        },
+        "id": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "identifier": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "if": {
+          "$ref": "#/definitions/commonOptions/if"
+        },
+        "key": {
+          "$ref": "#/definitions/commonOptions/key"
+        },
+        "label": {
+          "$ref": "#/definitions/commonOptions/label"
+        },
+        "name": {
+          "$ref": "#/definitions/commonOptions/label"
+        },
+        "prompt": {
+          "$ref": "#/definitions/commonOptions/prompt"
+        },
+        "type": {
+          "type": "string",
+          "enum": [ "input" ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "nestedInputStep": {
+      "type": "object",
+      "properties": {
+        "input": {
+          "$ref": "#/definitions/inputStep"
+        }
+      },
+      "additionalProperties": false
+    },
+    "stringInputStep": {
+      "type": "string",
+      "description": "Pauses the execution of a build and waits on a user to unblock it",
+      "enum": [ "input" ]
+    },
+    "commandStep": {
+      "type": "object",
+      "properties": {
+        "agents": {
+          "$ref": "#/definitions/commonOptions/agents"
+        },
+        "allow_dependency_failure": {
+          "$ref": "#/definitions/commonOptions/allowDependencyFailure"
+        },
+        "artifact_paths": {
+          "anyOf": [
+            {
+              "type": "string"
+            },
+            {
+              "type": "array",
+              "items": { "type": "string" }
+            }
+          ],
+          "description": "The glob path/s of artifacts to upload once this step has finished running",
+          "examples": [
+            [ "screenshots/*" ],
+            [ "dist/myapp.zip", "dist/myapp.tgz" ]
+          ]
+        },
+        "branches": {
+          "$ref": "#/definitions/commonOptions/branches"
+        },
+        "command": {
+          "description": "The commands to run on the agent",
+          "anyOf": [
+            { "type": "array", "items": { "type": "string" } },
+            { "type": "string" }
+          ]
+        },
+        "commands": {
+          "description": "The commands to run on the agent",
+          "$ref": "#/definitions/commandStep/properties/command"
+        },
+        "concurrency": {
+          "type": "integer",
+          "description": "The maximum number of jobs created from this step that are allowed to run at the same time. If you use this attribute, you must also define concurrency_group.",
+          "examples": [
+            1
+          ]
+        },
+        "concurrency_group": {
+          "type": "string",
+          "description": "A unique name for the concurrency group that you are creating with the concurrency attribute",
+          "examples": [
+            "my-pipeline/deploy"
+          ]
+        },
+        "concurrency_method": {
+          "type": "string",
+          "enum": ["ordered", "eager"],
+          "description": "Control command order, allowed values are 'ordered' (default) and 'eager'.  If you use this attribute, you must also define concurrency_group and concurrency.",
+          "examples": [
+            "ordered"
+          ]
+        },
+        "depends_on": {
+          "$ref": "#/definitions/commonOptions/dependsOn"
+        },
+        "env": {
+          "$ref": "#/definitions/commonOptions/env"
+        },
+        "id": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "identifier": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "if": {
+          "$ref": "#/definitions/commonOptions/if"
+        },
+        "key": {
+          "$ref": "#/definitions/commonOptions/key"
+        },
+        "label": {
+          "$ref": "#/definitions/commonOptions/label"
+        },
+        "matrix": {
+          "oneOf": [
+            {
+              "type": "array",
+              "description": "List of elements for simple single-dimension Build Matrix",
+              "items": { "$ref": "#/definitions/commonOptions/matrixElement" },
+              "examples": [
+                ["linux", "freebsd"]
+              ]
+            },
+            {
+              "type": "object",
+              "description": "Configuration for multi-dimension Build Matrix",
+              "properties": {
+                "setup": {
+                  "oneOf": [
+                    {
+                      "type": "array",
+                      "description": "List of elements for single-dimension Build Matrix",
+                      "items": { "$ref": "#/definitions/commonOptions/matrixElement" },
+                      "examples": [
+                        ["linux", "freebsd"]
+                      ]
+                    },
+                    {
+                      "type": "object",
+                      "description": "Mapping of Build Matrix dimension names to their lists of elements",
+                      "propertyNames": {
+                        "type": "string",
+                        "description": "Build Matrix dimension name",
+                        "pattern": "^[a-zA-Z0-9_]+$"
+                      },
+                      "additionalProperties": {
+                        "type": "array",
+                        "description": "List of elements for this Build Matrix dimension",
+                        "items": { "$ref": "#/definitions/commonOptions/matrixElement" }
+                      },
+                      "examples": [
+                        {
+                          "os": ["linux", "freebsd"],
+                          "arch": ["arm64", "riscv"]
+                        }
+                      ]
+                    }
+                  ]
+                },
+                "adjustments": {
+                  "type": "array",
+                  "description": "List of Build Matrix adjustments",
+                  "items": {
+                    "type": "object",
+                    "description": "An adjustment to a Build Matrix",
+                    "properties": {
+                      "with": {
+                        "oneOf": [
+                          {
+                            "type": "array",
+                            "description": "List of existing or new elements for single-dimension Build Matrix",
+                            "items": { "$ref": "#/definitions/commonOptions/matrixElement" }
+                          },
+                          {
+                            "type": "object",
+                            "description": "Specification of a new or existing Build Matrix combination",
+                            "propertyNames": {
+                              "type": "string",
+                              "description": "Build Matrix dimension name"
+                            },
+                            "additionalProperties": {
+                              "type": "string",
+                              "description": "Build Matrix dimension element"
+                            },
+                            "examples": [
+                              { "os": "linux", "arch": "arm64" }
+                            ]
+                          }
+                        ]
+                      },
+                      "skip": {
+                        "$ref": "#/definitions/commonOptions/skip"
+                      },
+                      "soft_fail": {
+                        "$ref": "#/definitions/commonOptions/softFail"
+                      }
+                    },
+                    "required": ["with"]
+                  }
+                }
+              },
+              "required": ["setup"]
+            }
+          ]
+        },
+        "name": {
+          "$ref": "#/definitions/commonOptions/label"
+        },
+        "notify": {
+          "type": "array",
+          "description": "Array of notification options for this step",
+          "items": {
+            "oneOf": [
+              {
+                "type": "string",
+                "enum": ["github_check", "github_commit_status"]
+              },
+              {
+                "type": "object",
+                "properties": {
+                  "basecamp_campfire": {
+                    "type": "string"
+                  },
+                  "if": {
+                    "$ref": "#/definitions/commonOptions/if"
+                  }
+                },
+                "additionalProperties": false
+              },
+              {
+                "type": "object",
+                "properties": {
+                  "slack": {
+                    "oneOf": [
+                      {
+                        "type": "string"
+                      },
+                      {
+                        "type": "object",
+                        "properties": {
+                          "channels": {
+                            "type": "array",
+                            "items": {
+                              "type": "string"
+                            }
+                          },
+                          "message": {
+                            "type": "string"
+                          }
+                        }
+                      }
+                    ]
+                  },
+                  "if": {
+                    "$ref": "#/definitions/commonOptions/if"
+                  }
+                },
+                "additionalProperties": false
+              },
+              {
+                "type": "object",
+                "properties": {
+                  "github_commit_status": {
+                    "type": "object",
+                    "properties": {
+                      "context": {
+                        "description": "GitHub commit status name",
+                        "type": "string"
+                      }
+                    }
+                  },
+                  "if": {
+                    "$ref": "#/definitions/commonOptions/if"
+                  }
+                },
+                "additionalProperties": false
+              },
+              {
+                "type": "object",
+                "properties": {
+                  "github_check": {
+                    "type": "object",
+                    "properties": {
+                      "context": {
+                        "description": "GitHub commit status name",
+                        "type": "string"
+                      }
+                    }
+                  },
+                  "if": {
+                    "$ref": "#/definitions/commonOptions/if"
+                  }
+                },
+                "additionalProperties": false
+              }
+            ]
+          }
+        },
+        "parallelism": {
+          "type": "integer",
+          "description": "The number of parallel jobs that will be created based on this step",
+          "examples": [
+            42
+          ]
+        },
+        "plugins": {
+          "anyOf": [
+            {
+              "type": "array",
+              "description": "Array of plugins for this step",
+              "items": {
+                "oneOf": [
+                  {
+                    "type": "string"
+                  },
+                  {
+                    "type": "object",
+                    "maxProperties": 1,
+                    "examples": [
+                      { "docker-compose#v1.0.0": { "run": "app" } }
+                    ]
+                  }
+                ]
+              }
+            },
+            {
+              "type": "object",
+              "description": "A map of plugins for this step. Deprecated: please use the array syntax."
+            }
+          ]
+        },
+        "soft_fail": {
+          "$ref": "#/definitions/commonOptions/softFail"
+        },
+        "retry": {
+          "type": "object",
+          "description": "The conditions for retrying this step.",
+          "properties": {
+            "automatic": {
+              "anyOf": [
+                {
+                  "type": "boolean"
+                },
+                {
+                  "$ref": "#/definitions/commonOptions/automaticRetry"
+                },
+                {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/definitions/commonOptions/automaticRetry"
+                  }
+                }
+              ],
+              "description": "Whether to allow a job to retry automatically. If set to true, the retry conditions are set to the default value.",
+              "default": [
+                {
+                  "exit_status": "*",
+                  "limit": 2
+                }
+              ]
+            },
+            "manual": {
+              "description": "Whether to allow a job to be retried manually",
+              "anyOf": [
+                {
+                  "type": "boolean"
+                },
+                {
+                  "type": "object",
+                  "properties": {
+                    "allowed": {
+                      "type": "boolean",
+                      "description": "Whether or not this job can be retried manually",
+                      "default": true
+                    },
+                    "permit_on_passed": {
+                      "type": "boolean",
+                      "description": "Whether or not this job can be retried after it has passed",
+                      "default": true
+                    },
+                    "reason": {
+                      "type": "string",
+                      "description": "A string that will be displayed in a tooltip on the Retry button in Buildkite. This will only be displayed if the allowed attribute is set to false.",
+                      "examples": [
+                        "No retries allowed on deploy steps"
+                      ]
+                    }
+                  },
+                  "additionalProperties": false
+                }
+              ]
+            }
+          }
+        },
+        "skip": {
+          "$ref": "#/definitions/commonOptions/skip"
+        },
+        "timeout_in_minutes": {
+          "type": "integer",
+          "description": "The number of minutes to time out a job",
+          "minimum": 1,
+          "examples": [
+            60
+          ]
+        },
+        "type": {
+          "type": "string",
+          "enum": [ "script", "command", "commands" ]
+        },
+        "priority": {
+          "type": "integer",
+          "description": "Priority of the job, higher priorities are assigned to agents",
+          "examples": [
+            -1,
+            1
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "nestedCommandStep": {
+      "type": "object",
+      "properties": {
+        "command": { "$ref": "#/definitions/commandStep" },
+        "commands": { "$ref": "#/definitions/commandStep" },
+        "script": { "$ref": "#/definitions/commandStep" }
+      },
+      "additionalProperties": false
+    },
+    "stringWaitStep": {
+      "type": "string",
+      "description": "Waits for previous steps to pass before continuing",
+      "enum": [ "wait", "waiter" ]
+    },
+    "waitStep": {
+      "type": "object",
+      "properties": {
+        "allow_dependency_failure": {
+          "$ref": "#/definitions/commonOptions/allowDependencyFailure"
+        },
+        "continue_on_failure": {
+          "description": "Continue to the next steps, even if the previous group of steps fail",
+          "type": "boolean"
+        },
+        "depends_on": {
+          "$ref": "#/definitions/commonOptions/dependsOn"
+        },
+        "id": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "identifier": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "if": {
+          "$ref": "#/definitions/commonOptions/if"
+        },
+        "key": {
+          "$ref": "#/definitions/commonOptions/key"
+        },
+        "type": {
+          "type": "string",
+          "enum": [ "wait", "waiter" ]
+        },
+        "wait": {
+          "description": "Waits for previous steps to pass before continuing",
+          "anyOf": [
+            { "type": "null" },
+            { "type": "string", "enum": [ "" ] }
+          ]
+        },
+        "waiter": {
+          "anyOf": [
+            { "type": "null" },
+            { "type": "string", "enum": [ "" ] }
+          ]
+        }
+      },
+      "additionalProperties": false
+    },
+    "nestedWaitStep": {
+      "type": "object",
+      "properties": {
+        "wait": {
+          "description": "Waits for previous steps to pass before continuing",
+          "$ref": "#/definitions/waitStep"
+        },
+        "waiter": {
+          "$ref": "#/definitions/waitStep"
+        }
+      },
+      "additionalProperties": false
+    },
+    "triggerStep": {
+      "type": "object",
+      "properties": {
+        "allow_dependency_failure": {
+          "$ref": "#/definitions/commonOptions/allowDependencyFailure"
+        },
+        "async": {
+          "type": "boolean",
+          "default": false,
+          "description": "Whether to continue the build without waiting for the triggered step to complete"
+        },
+        "branches": {
+          "$ref": "#/definitions/commonOptions/branches"
+        },
+        "build": {
+          "type": "object",
+          "description": "Properties of the build that will be created when the step is triggered",
+          "properties": {
+            "branch": {
+              "type": "string",
+              "description": "The branch for the build",
+              "default": "master",
+              "examples": [
+                "master",
+                "feature/xyz"
+              ]
+            },
+            "commit": {
+              "type": "string",
+              "description": "The commit hash for the build",
+              "default": "HEAD",
+              "examples": [
+                "HEAD",
+                "b5fb108"
+              ]
+            },
+            "env": {
+              "$ref": "#/definitions/commonOptions/env"
+            },
+            "label": {
+              "$ref": "#/definitions/commonOptions/label"
+            },
+            "name": {
+              "$ref": "#/definitions/commonOptions/label"
+            },
+            "message": {
+              "type": "string",
+              "description": "The message for the build (supports emoji)",
+              "default": "The label of the trigger step",
+              "examples": [
+                "Deployment 123 :rocket:"
+              ]
+            },
+            "meta_data": {
+              "type": "object",
+              "description": "Meta-data for the build",
+              "examples": [
+                { "server": "i-b244e37160c" }
+              ]
+            },
+            "trigger": {
+              "type": "string",
+              "description": "The slug of the pipeline to create a build",
+              "examples": [
+                "myapp-deploy"
+              ]
+            },
+            "type": {
+              "type": "string",
+              "enum": [ "trigger" ]
+            }
+          },
+          "additionalProperties": false
+        },
+        "depends_on": {
+          "$ref": "#/definitions/commonOptions/dependsOn"
+        },
+        "id": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "identifier": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "if": {
+          "$ref": "#/definitions/commonOptions/if"
+        },
+        "key": {
+          "$ref": "#/definitions/commonOptions/key"
+        },
+        "label": {
+          "$ref": "#/definitions/commonOptions/label"
+        },
+        "name": {
+          "$ref": "#/definitions/commonOptions/label"
+        },
+        "type": {
+          "type": "string",
+          "enum": [ "trigger" ]
+        },
+        "trigger": {
+          "type": "string",
+          "description": "The slug of the pipeline to create a build"
+        },
+        "skip": {
+          "$ref": "#/definitions/commonOptions/skip"
+        }
+      },
+      "additionalProperties": false
+    },
+    "nestedTriggerStep": {
+      "type": "object",
+      "properties": {
+        "trigger": {
+          "$ref": "#/definitions/triggerStep"
+        }
+      },
+      "additionalProperties": false
+    },
+    "groupStep": {
+      "properties": {
+        "depends_on": {
+          "$ref": "#/definitions/commonOptions/dependsOn"
+        },
+        "group": {
+          "type": "string",
+          "description": "The name to give to this group of steps",
+          "examples": [ "Tests" ]
+        },
+        "id": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "identifier": {
+          "$ref": "#/definitions/commonOptions/identifier"
+        },
+        "key": {
+          "$ref": "#/definitions/commonOptions/key"
+        },
+        "label": {
+          "$ref": "#/definitions/groupStep/properties/group"
+        },
+        "name": {
+          "$ref": "#/definitions/groupStep/properties/label"
+        },
+        "steps": {
+          "type": "array",
+          "description": "A list of steps",
+          "items": {
+            "anyOf": [
+              { "$ref": "#/definitions/stringBlockStep" },
+              { "$ref": "#/definitions/blockStep" },
+              { "$ref": "#/definitions/commandStep" },
+              { "$ref": "#/definitions/nestedCommandStep" },
+              { "$ref": "#/definitions/triggerStep" },
+              { "$ref": "#/definitions/nestedTriggerStep" },
+              { "$ref": "#/definitions/stringWaitStep" },
+              { "$ref": "#/definitions/waitStep" },
+              { "$ref": "#/definitions/nestedWaitStep" }
+            ]
+          },
+          "minSize": 1
+        },
+        "type": {
+          "type": "string",
+          "enum": [ "group" ]
+        }
+      },
+      "additionalProperties": false
+    }
+  },
+  "properties": {
+    "env": {
+      "$ref": "#/definitions/commonOptions/env"
+    },
+    "agents": {
+      "$ref": "#/definitions/commonOptions/agents"
+    },
+    "notify": {
+      "$ref": "#/definitions/commonOptions/buildNotify"
+    },
+    "steps": {
+      "description": "A list of steps",
+      "type": "array",
+      "items": {
+        "anyOf": [
+          { "$ref": "#/definitions/blockStep" },
+          { "$ref": "#/definitions/nestedBlockStep" },
+          { "$ref": "#/definitions/stringBlockStep" },
+          { "$ref": "#/definitions/inputStep" },
+          { "$ref": "#/definitions/nestedInputStep" },
+          { "$ref": "#/definitions/stringInputStep" },
+          { "$ref": "#/definitions/commandStep" },
+          { "$ref": "#/definitions/nestedCommandStep" },
+          { "$ref": "#/definitions/stringWaitStep" },
+          { "$ref": "#/definitions/waitStep" },
+          { "$ref": "#/definitions/nestedWaitStep" },
+          { "$ref": "#/definitions/triggerStep" },
+          { "$ref": "#/definitions/nestedTriggerStep" },
+          { "$ref": "#/definitions/groupStep" }
+        ]
+      }
+    }
+  }
+}
diff --git a/src/check_jsonschema/builtin_schemas/vendor/buildkite.sha256 b/src/check_jsonschema/builtin_schemas/vendor/buildkite.sha256
new file mode 100644
index 0000000..6bd6740
--- /dev/null
+++ b/src/check_jsonschema/builtin_schemas/vendor/buildkite.sha256
@@ -0,0 +1,1 @@
+9c400f617d809ea2065b62a67f91462aa4ab31d2601940a02adc0d08cbd8a11c
\ No newline at end of file
diff --git a/src/check_jsonschema/catalog.py b/src/check_jsonschema/catalog.py
index 89b66cd..636d62e 100644
--- a/src/check_jsonschema/catalog.py
+++ b/src/check_jsonschema/catalog.py
@@ -38,6 +38,24 @@ SCHEMA_CATALOG: dict[str, dict[str, t.Any]] = {
             "types": "yaml",
         },
     },
+    "buildkite": {
+        "url": _githubusercontent_url(
+            "buildkite", "pipeline-schema", "main", "schema.json"
+        ),
+        "hook_config": {
+            "name": "Validate Buildkite Pipelines",
+            "description": (
+                "Validate Buildkite Pipelines against the schema provided by Buildkite"
+            ),
+            "files": [
+                r"buildkite\.(yml|yaml|json)",
+                r"buildkite\.(.+)\.(yml|yaml|json)",
+                r"(.*/)?\.buildkite/pipeline\.(yml|yaml|json)",
+                r"(.*/)?\.buildkite/pipeline\.(.+)\.(yml|yaml|json)",
+            ],
+            "types_or": ["json", "yaml"],
+        },
+    },
     "dependabot": {
         "url": "https://json.schemastore.org/dependabot-2.0.json",
         "hook_config": {


Test patch:
diff --git a/tests/example-files/hooks/positive/buildkite/matrix.yml b/tests/example-files/hooks/positive/buildkite/matrix.yml
new file mode 100644
index 0000000..4a5d771
--- /dev/null
+++ b/tests/example-files/hooks/positive/buildkite/matrix.yml
@@ -0,0 +1,57 @@
+# this example copied from the buildkite pipeline-schema repo
+# it is therefore licensed under the existing buildkite MIT license
+# see src/check_jsonschema/builtin_schemas/vendor/ for license details
+
+steps:
+  - command: "echo {{matrix}}"
+    label: "{{matrix}}"
+    matrix:
+      - one
+      - two
+
+  - command: "echo {{matrix}}"
+    label: "{{matrix}}"
+    matrix:
+      setup:
+        - one
+        - two
+      adjustments:
+        - with: ["three"]
+          skip: true
+
+  - command: "echo {{matrix.color}} {{matrix.shape}}"
+    label: "{{matrix.color}} {{matrix.shape}}"
+    matrix:
+      setup:
+        color:
+          - green
+          - blue
+        shape:
+          - triangle
+          - hexagon
+      adjustments:
+        - with: {color: blue, shape: triangle}
+          skip: true
+        - with: {color: green, shape: triangle}
+          skip: "look, hexagons are just better"
+        - with: {color: purple, shape: octagon}
+
+
+  - group: matrices
+    steps:
+      - command: "echo {{matrix}}"
+        label: "{{matrix}}"
+        matrix:
+          - one
+          - two
+
+      - command: "echo {{matrix.color}} {{matrix.shape}}"
+        label: "{{matrix.color}} {{matrix.shape}}"
+        matrix:
+          setup:
+            color:
+              - green
+              - blue
+            shape:
+              - triangle
+              - hexagon