openova/.github/workflows/check-chart-annotations.yaml
e3mrah a9476b93f2
ci: elevate smoke-render guard to pre-merge (prevents dual-annotation PR-N dead-reserve) (#2093)
Trigger: bp-network-policies:1.0.1 dead-reserved 2026-05-20. The chart
had `catalyst.openova.io/no-upstream: "true"` (passing the pre-merge
GUARD 1 elevated in PR #2087 / TBD-V35) but was missing
`catalyst.openova.io/smoke-render-mode: "default-off"`. Its
`enabled: false` master gate rendered 1 line at default values, tripping
the post-merge smoke-render guard. By then the version in Chart.yaml
was already on main; recovery required a follow-up bump-and-fix PR.

Same shape as PR #2087; this PR closes the dual-annotation gap so the
second annotation slipping through also fails pre-merge.

What this PR does
-----------------

- scripts/check-chart-annotations.sh — extended with GUARD 2:
    For every chart Chart.yaml passed in (default: every
    platform/*/chart/Chart.yaml + products/*/chart/Chart.yaml under the
    repo): run `helm template <chart-dir>` at default values. If output
    is <5 lines AND the chart lacks the smoke-render-mode:default-off
    annotation, FAIL with operator guidance pointing at
    docs/BLUEPRINT-AUTHORING.md §11. For charts with non-empty
    `dependencies:`, run `helm dependency build` first (registry-auth
    pre-configured by the workflow).

    GUARD 1 logic preserved unchanged.

    New env knob: SKIP_SMOKE_RENDER=1 for local dev runs without GHCR
    pull token; CI never sets this.

- .github/workflows/check-chart-annotations.yaml — added:
    - azure/setup-helm@v4 step (same pin as blueprint-release.yaml)
    - GHCR helm registry login (read-only, packages: read perm)
    - timeout raised 5 → 10 min to accommodate helm dep build

- docs/BLUEPRINT-AUTHORING.md — Guard table rewritten to show both
  pre-merge guards (GUARD 1 + GUARD 2) above the post-merge belt-and-
  braces guards.

Validation
----------

Positive tests (local):
  - bp-network-policies:1.0.2 (both annotations present, 1-line render)
    → PASS
  - axon:0.1.0 (no-upstream:true, 277-line render)         → PASS
  - bp-kyverno-policies:1.0.0 (no-upstream:true, 1167-line) → PASS

Negative test (local):
  - Strip smoke-render-mode:default-off from
    bp-network-policies:1.0.2 → guard fails with exit 1 and the
    operator-guidance error message pointing at the annotation +
    BLUEPRINT-AUTHORING.md.

The post-merge guard in .github/workflows/blueprint-release.yaml stays
in place as belt-and-braces (same logic, same annotation key); pre-
merge catches the violation while the version in Chart.yaml is still
editable.

Refs #2092 (TBD-V38)
Refs #2086 (TBD-V35 — sibling GUARD 1 elevation, PR #2087)
Refs #2080 (TBD-V34 — bp-continuum dead-reserve)

Co-authored-by: hatiyildiz <hati.yildiz@openova.io>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:24:14 +04:00

180 lines
7.0 KiB
YAML

name: Chart-annotations guard (pre-merge hollow-chart check)
# PRE-MERGE replica of GUARD 1 + GUARD 2 in
# .github/workflows/blueprint-release.yaml.
#
# Catches hollow-chart violations BEFORE the PR merges:
#
# GUARD 1 — Chart.yaml has NO `dependencies:` entry AND no
# `catalyst.openova.io/no-upstream: "true"` opt-out annotation.
# (Elevated to pre-merge in PR #2087 / TBD-V35.)
#
# GUARD 2 — Default-values `helm template` of the chart produces <5 lines
# AND the chart lacks the
# `catalyst.openova.io/smoke-render-mode: default-off` annotation.
# (Elevated to pre-merge in this PR / TBD-V38.)
#
# Without these gates, violations only surface at the post-merge Blueprint
# Release workflow — by which point the version in Chart.yaml is
# "dead-reserved" (the merge SHA owns it but no GHCR tag ever publishes)
# and recovery requires a follow-up version-bump-and-annotate PR.
#
# Recurrence history that motivated promoting these guards to pre-merge:
# GUARD 1:
# - bp-cert-manager:1.0.0 (issue #181 — guard origin)
# - bp-crossplane-claims (historical)
# - bp-kyverno-policies (PR #2023)
# - bp-continuum:0.1.1 (PR #2072 dead-pinned, fix PR #2081, TBD-V34 / #2080)
# GUARD 2:
# - bp-network-policies:1.0.1 (had no-upstream:true but missing
# smoke-render-mode; dead-reserved 2026-05-20 — required BOTH
# annotations. The dual-annotation gap motivated this elevation.)
#
# Per CLAUDE.md anti-pattern catalogue + Inviolable Principle #13
# (chart-pin bumps must match a published GHCR tag): every dead-reserved
# version is a chart-pin lockstep break.
#
# Per CLAUDE.md "every workflow MUST be event-driven, NEVER scheduled":
# this workflow is push-on-merge (belt-and-braces) + pull_request-on-touch.
# There is no `schedule:` trigger; ad-hoc reruns go through
# workflow_dispatch.
#
# Scoping note — only CHANGED charts are checked in PRs. Pre-existing
# violations are NOT blocked by this guard until a PR actually touches the
# chart; the post-merge Blueprint Release workflow continues to fail-loudly
# on their next publish attempt regardless. This keeps the guard zero-noise
# for unrelated PRs while still catching every new chart introduction or
# version-bump that would dead-reserve a tag.
on:
pull_request:
paths:
- 'platform/*/chart/Chart.yaml'
- 'products/*/chart/Chart.yaml'
- 'scripts/check-chart-annotations.sh'
- '.github/workflows/check-chart-annotations.yaml'
push:
branches: [main]
paths:
- 'platform/*/chart/Chart.yaml'
- 'products/*/chart/Chart.yaml'
- 'scripts/check-chart-annotations.sh'
- '.github/workflows/check-chart-annotations.yaml'
workflow_dispatch:
inputs:
scope:
description: 'Scope: changed (PR diff) or all (every chart in the tree)'
required: false
type: choice
default: changed
options:
- changed
- all
permissions:
contents: read
# GUARD 2 needs to `helm dependency build` against
# oci://ghcr.io/openova-io/bp-* subcharts. Read-only GHCR pull
# token is sufficient; the post-merge workflow uses the same scope.
packages: read
jobs:
check:
name: Chart-annotations guard
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Need both sides of the PR diff to enumerate changed charts.
# PR runs already get `refs/pull/N/merge` with 2 commits; push
# runs need a depth >= 2 so HEAD~1 resolves.
fetch-depth: 2
- name: Set up Helm
# GUARD 2 needs `helm template` (and `helm dependency build` for
# charts with declared dependencies). Pin matches the post-merge
# Blueprint Release workflow.
uses: azure/setup-helm@v4
with:
version: v3.18.4
- name: Install yq (declared-deps parser)
run: |
# Same yq pin as the post-merge Blueprint Release workflow —
# awk/grep on YAML is fragile and would let a subtly malformed
# Chart.yaml slip past the guard. Keep the version in sync with
# .github/workflows/blueprint-release.yaml.
sudo wget -qO /usr/local/bin/yq \
https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
yq --version
- name: Helm registry login (for OCI subchart resolution)
# `helm dependency build` resolves `oci://ghcr.io/openova-io/bp-*`
# subcharts; needs an authenticated helm registry login. Read-only
# GITHUB_TOKEN with `packages: read` (above) is sufficient.
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io \
--username "${{ github.actor }}" --password-stdin
- name: Detect changed chart manifests
id: changed
run: |
set -euo pipefail
# workflow_dispatch with scope=all → run over every chart.
if [ "${{ github.event_name }}" = "workflow_dispatch" ] \
&& [ "${{ inputs.scope }}" = "all" ]; then
echo "scope=all" >> "$GITHUB_OUTPUT"
echo "charts=" >> "$GITHUB_OUTPUT"
exit 0
fi
# PR runs: compare against the merge base.
# push-to-main runs: compare against the previous commit.
if [ "${{ github.event_name }}" = "pull_request" ]; then
base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.event.pull_request.head.sha }}"
# actions/checkout@v4 doesn't fetch the base by default for
# shallow clones; fetch just enough to diff.
git fetch --no-tags --depth=1 origin "$base_sha" 2>/dev/null || true
range="${base_sha}...${head_sha}"
else
range="HEAD~1...HEAD"
fi
echo "Diffing range: $range"
changed=$(git diff --name-only "$range" 2>/dev/null \
| grep -E '^(platform|products)/[^/]+/chart/Chart\.yaml$' \
| sort -u || true)
echo "Changed Chart.yaml files:"
echo "$changed"
# Multi-line outputs need the EOF-heredoc form.
{
echo "scope=changed"
echo "charts<<EOF"
echo "$changed"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Run hollow-chart guards (GUARD 1 + GUARD 2)
run: |
set -euo pipefail
if [ "${{ steps.changed.outputs.scope }}" = "all" ]; then
echo "Scope: all (workflow_dispatch override)"
bash scripts/check-chart-annotations.sh
exit $?
fi
# Scope: changed. Empty list = no chart manifests touched → skip.
charts="${{ steps.changed.outputs.charts }}"
if [ -z "$charts" ]; then
echo "No Chart.yaml files changed in this PR — guard skipped."
exit 0
fi
# shellcheck disable=SC2086
echo "$charts" | xargs -r bash scripts/check-chart-annotations.sh