From faedecb7be237e3bdd16b87c467544fb9896e600 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Pi=C4=85tkowski?= Date: Wed, 5 Aug 2026 13:19:56 +0200 Subject: [PATCH 1/3] feat(ci): self-test json-schema-validation against a fixture chart Renovate bumped `verify-helm-schema` to v2.6.2 (#258) and broke JSON schema validation in every cluster app repo. schemalint's release pipeline had moved to architect 9.x, which publishes raw binaries instead of tarballs, while its action still relied on install-binary-action's default tarball URL - so the install 404'd. This repo is consumed at `@main`, so the breakage was live org-wide about a minute after merge. None of the existing checks could have caught it: yamllint, action-validator and zizmor are all static and never execute the pinned actions. Add a check that calls the real reusable workflow against a fixture chart, so the pins under test are exactly the ones that ship. The fixture schema comes from schemalint's own cluster-app testdata, normalized as `verify` requires, with values.yaml generated by helm-values-gen so the `generate` job's diff passes. Co-Authored-By: Claude Opus 5 --- .github/workflows/json-schema-validation.yaml | 4 + .../selftest-json-schema-validation.yaml | 29 ++ CHANGELOG.md | 10 + helm/selftest-cluster-app/README.md | 20 + helm/selftest-cluster-app/values.schema.json | 471 ++++++++++++++++++ helm/selftest-cluster-app/values.yaml | 64 +++ renovate.json5 | 4 + 7 files changed, 602 insertions(+) create mode 100644 .github/workflows/selftest-json-schema-validation.yaml create mode 100644 helm/selftest-cluster-app/README.md create mode 100644 helm/selftest-cluster-app/values.schema.json create mode 100644 helm/selftest-cluster-app/values.yaml diff --git a/.github/workflows/json-schema-validation.yaml b/.github/workflows/json-schema-validation.yaml index 25b9606..8c02b18 100644 --- a/.github/workflows/json-schema-validation.yaml +++ b/.github/workflows/json-schema-validation.yaml @@ -19,6 +19,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Run schemalint id: run-schemalint uses: giantswarm/schemalint/actions/verify-helm-schema@43a8314fa06020e2651cffc1617f5262b3d921a4 # v2.6.3 @@ -32,6 +34,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Run helm-values-gen id: run-helm-values-gen diff --git a/.github/workflows/selftest-json-schema-validation.yaml b/.github/workflows/selftest-json-schema-validation.yaml new file mode 100644 index 0000000..a6cd185 --- /dev/null +++ b/.github/workflows/selftest-json-schema-validation.yaml @@ -0,0 +1,29 @@ +# Self-test for `json-schema-validation.yaml`. +# +# That workflow is consumed org-wide at `@main`, so a bad action pin merged here is live in +# every cluster app repo within a minute. The repo's other checks are static (yamllint, +# action-validator, zizmor) and cannot see a broken action: schemalint v2.6.2 changed its +# release assets from tarballs to raw binaries, its action kept using install-binary-action's +# default tarball URL, and the resulting 404 only surfaced in consumer repos. +# +# This calls the real reusable workflow against the fixture chart in `helm/`, so the action +# pins under test are exactly the ones that ship - there is no second copy to drift. + +name: Self-test JSON schema validation + +on: + pull_request: + paths: + - '.github/workflows/json-schema-validation.yaml' + - '.github/workflows/selftest-json-schema-validation.yaml' + - 'helm/**' + workflow_dispatch: {} + +permissions: {} + +jobs: + selftest: + name: Run json-schema-validation against the fixture chart + uses: ./.github/workflows/json-schema-validation.yaml + permissions: + contents: read diff --git a/CHANGELOG.md b/CHANGELOG.md index e5d21d3..e8cf6c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), however this project does not use Semantic Versioning and there are no releases. Instead this file uses a date-based structure. +## 2026-08-05 + +### Added + +- `selftest-json-schema-validation.yaml` — new check that runs `json-schema-validation.yaml` itself against a fixture chart in `helm/selftest-cluster-app/` on pull requests that touch it. Renovate bumping `verify-helm-schema` to v2.6.2 broke every cluster app repo because schemalint's release assets changed from tarballs to raw binaries while its action still requested the tarball URL; nothing here could catch that, since this repo is consumed at `@main` and its only checks are static. + +### Changed + +- `json-schema-validation.yaml` — both `actions/checkout` steps now set `persist-credentials: false`, per the repository's action rules. Neither job uses git. + ## 2026-07-29 ### Fixed diff --git a/helm/selftest-cluster-app/README.md b/helm/selftest-cluster-app/README.md new file mode 100644 index 0000000..fd36a2f --- /dev/null +++ b/helm/selftest-cluster-app/README.md @@ -0,0 +1,20 @@ +# Self-test fixture chart + +Not a real chart, and not published anywhere. This exists only so +`.github/workflows/selftest-json-schema-validation.yaml` has something for +`json-schema-validation.yaml` to run against on pull requests. + +Both composite actions that workflow calls locate their input with +`find ./helm -maxdepth 2 -name values.schema.json`, and a `workflow_call` job cannot be given a +working directory, so the fixture has to sit at the repository root under `helm/`. + +- `values.schema.json` — copied from schemalint's own + `pkg/lint/rulesets/testdata/cluster_azure.json` (its `rulesets_test.go` asserts zero errors + under the `cluster-app` rule set), then passed through `schemalint normalize`, which `verify` + also requires. The ~90 recommendations it reports are advisory and do not fail the run. +- `values.yaml` — the exact stdout of `helm-values-gen values.schema.json`. The `generate` job + diffs the two, so regenerate this file whenever the schema changes: + + ```bash + helm-values-gen helm/selftest-cluster-app/values.schema.json > helm/selftest-cluster-app/values.yaml + ``` diff --git a/helm/selftest-cluster-app/values.schema.json b/helm/selftest-cluster-app/values.schema.json new file mode 100644 index 0000000..bf013bd --- /dev/null +++ b/helm/selftest-cluster-app/values.schema.json @@ -0,0 +1,471 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "title": "Cluster configuration", + "description": "Configuration of an Azure cluster using Cluster API", + "additionalProperties": false, + "properties": { + "baseDomain": { + "type": "string", + "title": "Base DNS domain", + "default": "azuretest.gigantic.io" + }, + "cluster-shared": { + "type": "object", + "title": "Library chart" + }, + "connectivity": { + "type": "object", + "title": "Connectivity", + "properties": { + "bastion": { + "type": "object", + "title": "Bastion host", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable bastion host for this cluster", + "default": true + }, + "instanceType": { + "type": "string", + "title": "VM size", + "description": "Type of virtual machine to use for the bastion host.", + "default": "Standard_D2s_v5" + } + } + }, + "network": { + "type": "object", + "title": "Network", + "properties": { + "controlPlane": { + "type": "object", + "title": "Control plane", + "properties": { + "cidr": { + "type": "string", + "title": "Subnet", + "default": "10.0.0.0/20" + } + } + }, + "hostCidr": { + "type": "string", + "title": "Node subnet", + "description": "IPv4 address range for nodes, in CIDR notation.", + "default": "10.0.0.0/16" + }, + "mode": { + "type": "string", + "title": "Network mode", + "description": "Specifying if the cluster resources are publicly accessible or not.", + "enum": [ + "public", + "private" + ], + "default": "public" + }, + "podCidr": { + "type": "string", + "title": "Pod subnet", + "description": "IPv4 address range for pods, in CIDR notation.", + "default": "192.168.0.0/16" + }, + "serviceCidr": { + "type": "string", + "title": "Service subnet", + "description": "IPv4 address range for services, in CIDR notation.", + "default": "172.31.0.0/16" + }, + "workers": { + "type": "object", + "title": "Workers", + "properties": { + "cidr": { + "type": "string", + "title": "Subnet", + "default": "10.0.16.0/20" + } + } + } + } + }, + "sshSSOPublicKey": { + "type": "string", + "title": "SSH Public key for single sign-on", + "default": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM4cvZ01fLmO9cJbWUj7sfF+NhECgy+Cl0bazSrZX7sU vault-ca@vault.operations.giantswarm.io" + } + } + }, + "controlPlane": { + "type": "object", + "title": "Control plane", + "properties": { + "etcdVolumeSizeGB": { + "type": "integer", + "title": "Etcd volume size (GB)", + "default": 10 + }, + "instanceType": { + "type": "string", + "title": "Node VM size", + "default": "Standard_D4s_v3" + }, + "oidc": { + "type": "object", + "title": "OIDC authentication", + "properties": { + "caPem": { + "type": "string", + "title": "Certificate authority", + "description": "Identity provider's CA certificate in PEM format.", + "default": "" + }, + "clientId": { + "type": "string", + "title": "Client ID", + "default": "" + }, + "groupsClaim": { + "type": "string", + "title": "Groups claim", + "default": "" + }, + "issuerUrl": { + "type": "string", + "title": "Issuer URL", + "default": "" + }, + "usernameClaim": { + "type": "string", + "title": "Username claim", + "default": "" + } + } + }, + "replicas": { + "type": "integer", + "title": "Number of nodes", + "default": 3 + }, + "rootVolumeSizeGB": { + "type": "integer", + "title": "Root volume size (GB)", + "default": 50 + } + } + }, + "internal": { + "type": "object", + "title": "Internal settings", + "properties": { + "defaults": { + "type": "object", + "title": "Default settings", + "properties": { + "evictionMinimumReclaim": { + "type": "string", + "title": "Default settings for eviction minimum reclaim", + "default": "imagefs.available=5%,memory.available=100Mi,nodefs.available=5%" + }, + "hardEvictionThresholds": { + "type": "string", + "title": "Default settings for hard eviction thresholds", + "default": "memory.available<200Mi,nodefs.available<10%,nodefs.inodesFree<3%,imagefs.available<10%,pid.available<20%" + }, + "softEvictionGracePeriod": { + "type": "string", + "title": "Default settings for soft eviction grace period", + "default": "memory.available=30s,nodefs.available=2m,nodefs.inodesFree=1m,imagefs.available=2m,pid.available=1m" + }, + "softEvictionThresholds": { + "type": "string", + "title": "Default settings for soft eviction thresholds", + "default": "memory.available<500Mi,nodefs.available<15%,nodefs.inodesFree<5%,imagefs.available<15%,pid.available<30%" + } + } + }, + "identity": { + "type": "object", + "title": "Identity", + "properties": { + "type": { + "type": "string", + "title": "Type of Identity", + "enum": [ + "SystemAssigned", + "UserAssigned" + ], + "default": "SystemAssigned" + }, + "attachCapzControllerUserAssignedIdentity": { + "type": "boolean", + "title": "Attach CAPZ controller UserAssigned identity", + "default": false + }, + "userAssignedCustomIdentities": { + "type": "array", + "title": "List of custom UserAssigned Identities to attach to all nodes", + "default": [] + } + } + }, + "image": { + "type": "object", + "title": "Node Image", + "properties": { + "gallery": { + "type": "string", + "title": "Gallery", + "description": "Name of the community gallery hosting the image", + "default": "gsCapzFlatcar-41c2d140-ac44-4d8b-b7e1-7b2f1ddbe4d0" + }, + "name": { + "type": "string", + "title": "Image Definition", + "description": "Name of the image definition in the Gallery", + "default": "" + }, + "version": { + "type": "string", + "title": "Image version", + "default": "3374.2.4" + } + } + }, + "kubernetesVersion": { + "type": "string", + "title": "Kubernetes version", + "default": "1.24.11" + }, + "network": { + "type": "object", + "title": "Network configuration", + "description": "Internal network configuration that is susceptible to more frequent change", + "properties": { + "vpn": { + "type": "object", + "title": "VPN configuration", + "description": "Internal VPN configuration that is susceptible to more frequent change", + "properties": { + "gatewayMode": { + "type": "string", + "title": "VPN gateway mode", + "enum": [ + "local", + "remote" + ], + "default": "remote" + } + } + } + } + } + } + }, + "managementCluster": { + "type": "string", + "title": "The capi MC managing this cluster" + }, + "metadata": { + "type": "object", + "title": "Metadata", + "properties": { + "description": { + "type": "string", + "title": "Cluster description", + "description": "User-friendly description of the cluster's purpose." + }, + "name": { + "type": "string", + "title": "Cluster name", + "description": "Unique identifier, cannot be changed after creation." + }, + "organization": { + "type": "string", + "title": "Organization" + }, + "servicePriority": { + "type": "string", + "title": "Service priority", + "description": "The relative importance of this cluster.", + "$comment": "Defined in https://github.com/giantswarm/rfc/tree/main/classify-cluster-priority", + "enum": [ + "highest", + "medium", + "lowest" + ], + "default": "highest" + } + } + }, + "nodePools": { + "type": "array", + "title": "Node pools", + "items": { + "type": "object", + "title": "Node pool", + "properties": { + "customNodeLabels": { + "type": "array", + "title": "Custom node labels", + "items": { + "type": "string", + "title": "Label" + } + }, + "customNodeTaints": { + "type": "array", + "title": "Custom node taints", + "items": { + "type": "object", + "title": "Node taint", + "required": [ + "effect", + "key", + "value" + ], + "properties": { + "effect": { + "type": "string", + "title": "Effect", + "enum": [ + "NoSchedule", + "PreferNoSchedule", + "NoExecute" + ] + }, + "key": { + "type": "string", + "title": "Key" + }, + "value": { + "type": "string", + "title": "Value" + } + } + }, + "descriptions": "Taints that will be set on all nodes in the node pool, to avoid the scheduling of certain workloads." + }, + "disableHealthCheck": { + "type": "boolean", + "title": "Disable HealthChecks for the MachineDeployment" + }, + "failureDomain": { + "type": "string", + "title": "Select zone where to deploy the nodePool", + "enum": [ + "1", + "2", + "3" + ] + }, + "instanceType": { + "type": "string", + "title": "VM size" + }, + "name": { + "type": "string", + "title": "Name", + "description": "Unique identifier, cannot be changed after creation." + }, + "replicas": { + "type": "integer", + "title": "Number of nodes" + }, + "rootVolumeSizeGB": { + "type": "integer", + "title": "Root volume size (GB)" + } + } + }, + "default": [ + { + "customNodeLabels": [], + "customNodeTaints": [], + "disableHealthCheck": false, + "instanceType": "Standard_D2s_v3", + "name": "md00", + "replicas": 3, + "rootVolumeSizeGB": 50 + } + ] + }, + "provider": { + "type": "string", + "title": "Cluster API provider name" + }, + "providerSpecific": { + "type": "object", + "title": "Azure settings", + "properties": { + "azureClusterIdentity": { + "type": "object", + "title": "Identity", + "description": "AzureClusterIdentity resource to use for this cluster.", + "properties": { + "name": { + "type": "string", + "title": "Name", + "default": "cluster-identity" + }, + "namespace": { + "type": "string", + "title": "Namespace", + "default": "org-giantswarm" + } + } + }, + "location": { + "type": "string", + "title": "Location", + "default": "westeurope" + }, + "network": { + "type": "object", + "title": "Azure network settings", + "description": "Azure VNet peering and other Azure-specific network settings.", + "additionalProperties": false, + "properties": { + "peerings": { + "type": "array", + "title": "VNet peerings", + "description": "Specifying VNets (their resource groups and names) to which the peering is established.", + "items": { + "type": "object", + "title": "VNet peering", + "additionalProperties": false, + "properties": { + "remoteVnetName": { + "type": "string", + "title": "VNet name", + "description": "Name of the remote VNet to which the peering is established.", + "maxLength": 64, + "minLength": 2, + "pattern": "^[-\\w\\._]+$" + }, + "resourceGroup": { + "type": "string", + "title": "Resource group name", + "description": "Resource group for the remote VNet to which the peering is established.", + "maxLength": 90, + "minLength": 1, + "pattern": "^[-\\w\\._\\(\\)]+$" + } + }, + "uniqueItems": true + }, + "default": [] + } + } + }, + "subscriptionId": { + "type": "string", + "title": "Subscription ID", + "default": "PLACEHOLDER" + } + } + } + } +} diff --git a/helm/selftest-cluster-app/values.yaml b/helm/selftest-cluster-app/values.yaml new file mode 100644 index 0000000..1c48365 --- /dev/null +++ b/helm/selftest-cluster-app/values.yaml @@ -0,0 +1,64 @@ +# Auto-generated by helm-values-gen, do not edit! + +baseDomain: azuretest.gigantic.io +connectivity: + bastion: + enabled: true + instanceType: Standard_D2s_v5 + network: + controlPlane: + cidr: 10.0.0.0/20 + hostCidr: 10.0.0.0/16 + mode: public + podCidr: 192.168.0.0/16 + serviceCidr: 172.31.0.0/16 + workers: + cidr: 10.0.16.0/20 + sshSSOPublicKey: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM4cvZ01fLmO9cJbWUj7sfF+NhECgy+Cl0bazSrZX7sU vault-ca@vault.operations.giantswarm.io +controlPlane: + etcdVolumeSizeGB: 10 + instanceType: Standard_D4s_v3 + oidc: + caPem: "" + clientId: "" + groupsClaim: "" + issuerUrl: "" + usernameClaim: "" + replicas: 3 + rootVolumeSizeGB: 50 +internal: + defaults: + evictionMinimumReclaim: imagefs.available=5%,memory.available=100Mi,nodefs.available=5% + hardEvictionThresholds: memory.available<200Mi,nodefs.available<10%,nodefs.inodesFree<3%,imagefs.available<10%,pid.available<20% + softEvictionGracePeriod: memory.available=30s,nodefs.available=2m,nodefs.inodesFree=1m,imagefs.available=2m,pid.available=1m + softEvictionThresholds: memory.available<500Mi,nodefs.available<15%,nodefs.inodesFree<5%,imagefs.available<15%,pid.available<30% + identity: + attachCapzControllerUserAssignedIdentity: false + type: SystemAssigned + userAssignedCustomIdentities: [] + image: + gallery: gsCapzFlatcar-41c2d140-ac44-4d8b-b7e1-7b2f1ddbe4d0 + name: "" + version: 3374.2.4 + kubernetesVersion: 1.24.11 + network: + vpn: + gatewayMode: remote +metadata: + servicePriority: highest +nodePools: + - customNodeLabels: [] + customNodeTaints: [] + disableHealthCheck: false + instanceType: Standard_D2s_v3 + name: md00 + replicas: 3 + rootVolumeSizeGB: 50 +providerSpecific: + azureClusterIdentity: + name: cluster-identity + namespace: org-giantswarm + location: westeurope + network: + peerings: [] + subscriptionId: PLACEHOLDER diff --git a/renovate.json5 b/renovate.json5 index e0e01b5..8ed7d24 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -3,6 +3,10 @@ extends: [ 'github>giantswarm/renovate-presets:default.json5', ], + // `helm/` holds a self-test fixture chart, not a real one. See helm/selftest-cluster-app/README.md. + ignorePaths: [ + 'helm/**', + ], customManagers: [ { description: 'Allow updating binaries in workflows installed via install-binary-action', From 43848fdf49e1db28d88796e3e3fb22ed1600ac3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Pi=C4=85tkowski?= Date: Wed, 5 Aug 2026 13:26:48 +0200 Subject: [PATCH 2/3] test: TEMPORARY negative test - pin schemalint back to broken v2.6.2 Proves the self-test actually catches the 404. Reverted in the next commit. Co-Authored-By: Claude Opus 5 --- .github/workflows/json-schema-validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/json-schema-validation.yaml b/.github/workflows/json-schema-validation.yaml index 8c02b18..2dde0ff 100644 --- a/.github/workflows/json-schema-validation.yaml +++ b/.github/workflows/json-schema-validation.yaml @@ -23,7 +23,7 @@ jobs: persist-credentials: false - name: Run schemalint id: run-schemalint - uses: giantswarm/schemalint/actions/verify-helm-schema@43a8314fa06020e2651cffc1617f5262b3d921a4 # v2.6.3 + uses: giantswarm/schemalint/actions/verify-helm-schema@c2280a5a7a2599b5987e726cdc03cf94e3c09f8b # v2.6.2 with: rule-set: 'cluster-app' generate: From 3beba60e567cb889c6f08c9c27f558b1c1532163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Pi=C4=85tkowski?= Date: Wed, 5 Aug 2026 13:40:57 +0200 Subject: [PATCH 3/3] Revert "test: TEMPORARY negative test - pin schemalint back to broken v2.6.2" Negative test done: run 31001492199 failed with the same "Unexpected HTTP response: 404" on schemalint-v2.6.2-linux-amd64.tar.gz that cluster-aks hit. Co-Authored-By: Claude Opus 5 --- .github/workflows/json-schema-validation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/json-schema-validation.yaml b/.github/workflows/json-schema-validation.yaml index 2dde0ff..8c02b18 100644 --- a/.github/workflows/json-schema-validation.yaml +++ b/.github/workflows/json-schema-validation.yaml @@ -23,7 +23,7 @@ jobs: persist-credentials: false - name: Run schemalint id: run-schemalint - uses: giantswarm/schemalint/actions/verify-helm-schema@c2280a5a7a2599b5987e726cdc03cf94e3c09f8b # v2.6.2 + uses: giantswarm/schemalint/actions/verify-helm-schema@43a8314fa06020e2651cffc1617f5262b3d921a4 # v2.6.3 with: rule-set: 'cluster-app' generate: