Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions .github/workflows/chart-provenance.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
name: chart provenance
name: upstream pins

# The live half of the chart-provenance check: is every chart this catalog pins
# still the chart it was pinned for, upstream, right now?
# The live half of every check that asks whether an upstream pin still resolves
# to what it was recorded against: charts, and the directories a directory-source
# Application asks the repo-server to combine.
#
# It is a separate workflow rather than a job in ci.yml on purpose, and there
# are two reasons.
#
# The verdict is not a function of this commit. A maintainer can deprecate a
# chart, or hand it to a different organisation, at any moment. Run in the merge
# path, that turns a pull request red for a reason the pull request did not
# cause — the same trap `mirror-check freshness` and `schemas:freshness` are
# kept off the blocking path to avoid.
# Neither verdict is a function of this commit. A maintainer can deprecate a
# chart, hand it to a different organisation, or move a tag, at any moment. Run
# in the merge path, that turns a pull request red for a reason the pull request
# did not cause — the same trap `mirror-check freshness` and `schemas:freshness`
# are kept off the blocking path to avoid. Resolving them also needs a clone or
# a registry round trip, which the merge path does not get to depend on.
#
# And the merge gate in ci.yml refuses any workflow containing a job it does not
# watch, while counting a skipped dependency as a failure. A job that only runs
# on a schedule cannot satisfy both, so it does not belong in that file. The
# offline half — every pin has a record, every record a pin — does run there,
# because that IS a function of the tree.
# offline halves — every pin has a record, every record a pin — do run there,
# because those ARE a function of the tree.
on:
schedule:
# Mondays, ahead of the working week and after the weekend's upstream releases.
Expand Down Expand Up @@ -54,3 +56,29 @@ jobs:
# "current" for something that is no longer the same software.
- name: Compare every pinned chart against its record
run: ./scripts/check-chart-deprecation.py --live

sizes:
name: pinned directory sources still measure what was recorded
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version-file: .python-version
- name: Install Python dependencies
run: pip install --require-hashes -r requirements.txt

# A directory source over the repo-server's combined-manifest ceiling
# generates nothing, and says so as a ComparisonError with the sync column
# reading Unknown rather than as anything that looks like a failure. The
# size is a function of (repoURL, targetRevision, path), so the blocking
# half in ci.yml already fails when a pin moves away from its measurement.
# What only a clone can answer is whether a tag that nobody moved here was
# moved upstream underneath it.
- name: Re-measure every pinned directory source
run: ./scripts/check-directory-manifest-size.py --live
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,20 @@ jobs:
./scripts/check-chart-deprecation.py
./scripts/check-chart-deprecation.py --self-test

# A source with a `path` and no chart is not automatically a kustomize
# directory: ArgoCD classifies by what the directory holds, and a Directory
# source is the one shape the repo-server measures against
# --max-combined-directory-manifests-size. Over that, it generates nothing
# and reports ComparisonError, which reads as `Unknown` in the sync column
# and surfaces waves later as whatever workload needed the kind that never
# installed. Offline like the chart half above: the sizes are keyed on
# (repoURL, targetRevision, path), so a pin cannot move without its
# measurement going stale here, and upstream/pins asks the live question.
- name: Directory sources fit the repo-server's combined-manifest ceiling
run: |
./scripts/check-directory-manifest-size.py
./scripts/check-directory-manifest-size.py --self-test

# ── Appset render gate ───────────────────────────────────────────────
# The Karpenter EC2NodeClass patch carries if/range control flow inside a
# `patch: |-` string block. Every other gate treats that string as opaque —
Expand Down
34 changes: 29 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ task validate:label-values # Every k8s label value satisfies the API ser
task validate:policy-admission # Prove no addon is denied by the Enforce-tier Kyverno policies (+ exclusion-list parity)
task validate:externalsecret-keys # Each ExternalSecret names its remote secret once, and the delivering appset patches it
task validate:secret-store-refs # Every secret-store reference names the one store this catalog declares, and the published contract states it
task validate:directory-manifest-size # Every directory-source Application fits the repo-server's combined-manifest ceiling
task validate:dashboards # grafana.com dashboard ids exist and are AMG-saveable
task validate:athena-panel-columns # Every column a CUR panel names is one the export delivers
task validate:fork-safety # No hardcoded catalog repoURL in applied ApplicationSets (report-only locally)
Expand All @@ -113,7 +114,7 @@ task validate:image-vulnerabilities # Every fixed CRITICAL in a rendered image i

`task validate` runs the structural gates (lint, kustomize build, helm-render,
ApplicationSet schema, sync-wave ordering, appset render, policy-admission,
secret-store references, dashboards, fork-safety). CI runs those plus several gates that have **no local
secret-store references, directory-source sizes, dashboards, fork-safety). CI runs those plus several gates that have **no local
`task` target**, and one that has a target but is deliberately outside the
aggregate, so a clean `task validate` is necessary but not sufficient:

Expand Down Expand Up @@ -178,6 +179,24 @@ aggregate, so a clean `task validate` is necessary but not sufficient:
delivered panel measures is a finding, because a figure compared to nothing is
how the last wrong one survived. The figure has no independent existence:
there is no constant to correct and none in the summary that anything trusts
- **Directory-source manifest sizes** — `scripts/check-directory-manifest-size.py`,
in the `appsets` job beside the offline chart-provenance half. The repo-server
refuses to generate a directory-type Application whose combined manifest files
exceed `--max-combined-directory-manifests-size`, and the Application then
reports `ComparisonError` with `Unknown` in the sync column — quieter than
`OutOfSync`, and the symptom arrives waves later as whatever workload needed
the kind that never installed. A source with a `path` is not automatically a
kustomize directory: ArgoCD takes an explicit `helm`/`kustomize`/`plugin`/
`directory` block at its word and otherwise classifies by what the directory
holds, so the population is derived by running that decision over the tree
rather than from a list, and a kustomization deleted out of an overlay
reclassifies that source into the corpus. The ceiling is not derivable here —
it is a repo-server flag, and this catalog installs no ArgoCD — so it is gated
rather than derived: `contracts/repo-server.json` records what the host must be
configured for, and the repository that configures the repo-server is where the
two are held equal. Sizes in `scripts/directory-sources.json` are keyed on
(repoURL, targetRevision, path), so a pin cannot move without its measurement
going stale and failing the blocking gate on the pull request that moves it
- **Image vulnerabilities** — `scripts/check-image-vulnerabilities.py` (CI job
`image-vulnerabilities`). The one with a target of its own,
`task validate:image-vulnerabilities`, kept out of the aggregate because it
Expand Down Expand Up @@ -206,10 +225,15 @@ documents: `task validate` runs it report-only, CI runs it `--blocking`.
- Standalone jobs on every PR: `helm-render` (templates every addon against its appset-pinned chart with base + each env's values — an unknown key fails here, not fleet-wide at sync), `policy-admission` (renders the whole fleet into its real destination namespaces and runs `kyverno apply` against the Enforce-tier best-practice/pod-security policies, so an addon landing in a namespace the policies don't exclude fails here instead of being denied at admission on a vended enforce cluster — also asserts all four exclusion lists stay identical, that every namespace the fleet lands a workload in is on that list, and that a deliberately non-compliant canary is denied by every rule, which is what proves the run evaluated anything), `appsets` (ApplicationSet schema + documented sync-wave ordering), `appset-render` (renders the Karpenter EC2NodeClass patch template the way the ArgoCD ApplicationSet controller does — Go text/template + sprig, `missingkey=error` — against fixture create/adopt/legacy cluster Secrets, so a control-flow edit that breaks the per-cluster render fails here instead of at sync), `secrets` (gitleaks over the working tree), plus the dashboard, fork-safety, and Kyverno policy gates
- Chart pins in `applicationsets/`, the Go module, the CI tool downloads and the GitHub Actions are all watched by Renovate (`renovate.json`, extending the org preset at `nanohype/.github`)
- A scheduled workflow, `.github/workflows/chart-provenance.yml`, runs weekly (Mondays)
and re-resolves every pinned chart against its recorded provenance via
`scripts/check-chart-deprecation.py --live` — so a chart that is deprecated,
moved, or no longer resolves to what it did at pin time surfaces on a schedule
rather than at the next sync
and re-resolves every upstream pin against what was recorded for it. One job
runs `scripts/check-chart-deprecation.py --live`, so a chart that is
deprecated, moved, or no longer resolves to what it did at pin time surfaces on
a schedule rather than at the next sync. The other runs
`scripts/check-directory-manifest-size.py --live`, which re-measures each
pinned directory source: the blocking half already fails when a pin moves away
from its measurement, and what only a clone can answer is whether a tag nobody
moved here was moved upstream underneath it. Both need the network, which is
why neither is on the merge path
- Manual diff rendering available via `.github/workflows/diff.yml`

## Claude Code Tooling
Expand Down
9 changes: 8 additions & 1 deletion Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,19 @@ tasks:
cmds:
- ./scripts/check-secret-store-refs.py

validate:directory-manifest-size:
desc: "Directory-source gate — every directory source fits the combined-manifest ceiling the repo-server is configured for"
cmds:
- ./scripts/check-directory-manifest-size.py
- ./scripts/check-directory-manifest-size.py --self-test

validate:athena-panel-columns:
desc: "Athena panel gate — every column a CUR panel names is one the export delivers"
cmds:
- ./scripts/check-athena-panel-columns.py

validate:
desc: Run all validations (lint, build, helm-render, appset schema, sync waves, label values, appset render, policy admission, secret refs, athena panels, dashboard/fork-safety)
desc: Run all validations (lint, build, helm-render, appset schema, sync waves, label values, appset render, policy admission, secret refs, directory sizes, athena panels, dashboard/fork-safety)
deps:
- lint:yaml
- lint:python
Expand All @@ -219,6 +225,7 @@ tasks:
- validate:policy-admission
- validate:externalsecret-keys
- validate:secret-store-refs
- validate:directory-manifest-size
- validate:athena-panel-columns
- validate:dashboards
- validate:fork-safety
Expand Down
7 changes: 7 additions & 0 deletions contracts/repo-server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"_purpose": "The argocd-repo-server this catalog is installed against must be configured to combine at least this many bytes of manifests for a directory-type Application, or the directory sources in applicationsets/ generate nothing and their Applications report ComparisonError. The value is not derivable from this repository: it is a repo-server flag, and this catalog installs no ArgoCD and pins no ArgoCD version. It is recorded here so the repository that does install ArgoCD can assert that what it configures and what this catalog needs are the same number.",
"_setBy": "argocd-repo-server --max-combined-directory-manifests-size, equivalently ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE; `argocd-repo-server --help` prints the flag and the default it ships with.",
"_measuredBy": "scripts/check-directory-manifest-size.py, which measures every directory source the way the repo-server does and fails before a cluster does. Sizes live in scripts/directory-sources.json.",
"maxCombinedDirectoryManifestsSize": "20M",
"argoCdDefault": "10M"
}
Loading