137 lines
7.2 KiB
YAML
137 lines
7.2 KiB
YAML
name: Validate Helm chart
|
|
|
|
on:
|
|
push:
|
|
paths:
|
|
- infra/helm/**
|
|
- .github/workflows/helm-chart.yml
|
|
pull_request:
|
|
paths:
|
|
- infra/helm/**
|
|
- .github/workflows/helm-chart.yml
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
validate:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- uses: azure/setup-helm@v4
|
|
with:
|
|
version: v3.17.3
|
|
- name: Lint chart
|
|
run: helm lint infra/helm/medical-chatbot
|
|
- name: Render defaults and check the immutable-tag guard
|
|
run: |
|
|
helm template default infra/helm/medical-chatbot > /tmp/default.yaml
|
|
|
|
# values-production.yaml turns authService/apiGateway on, and their
|
|
# jwt-secret Secret key is `required` with no default -- real values
|
|
# only ever exist inline on the live Application, never in Git (see
|
|
# the comment on secret.jwtSecret in values-production.yaml). This
|
|
# placeholder exists purely so these renders reach the assertion
|
|
# they're actually testing (the image-tag guard) instead of failing
|
|
# on an unrelated missing secret; it is never applied to a cluster.
|
|
CI_JWT_SECRET=ci-render-only-not-a-real-secret
|
|
|
|
# The live releases carry no image tag in Git -- it is supplied per
|
|
# deploy as a commit SHA through the ArgoCD Application. Rendering
|
|
# with an empty tag must FAIL rather than fall back to the chart's
|
|
# `local` development tag, so assert the failure directly; otherwise
|
|
# the guard could rot into a silent default unnoticed.
|
|
if helm template production infra/helm/medical-chatbot --values infra/helm/medical-chatbot/values-production.yaml --set secret.jwtSecret="$CI_JWT_SECRET" --set aiService.image.tag="" --set web.image.tag="" > /tmp/untagged.yaml 2>/tmp/untagged.err; then
|
|
echo "::error::render succeeded with no image tag; the immutable-tag guard is gone"
|
|
exit 1
|
|
fi
|
|
grep -q 'image.tag must be set to an immutable tag' /tmp/untagged.err
|
|
|
|
# ...and with a tag it must resolve the GHCR package, not the local
|
|
# development image name.
|
|
helm template production infra/helm/medical-chatbot --values infra/helm/medical-chatbot/values-production.yaml --set secret.jwtSecret="$CI_JWT_SECRET" --set aiService.image.repository=ghcr.io/baovu2k4/vsf-duocthu-ai-service --set web.image.repository=ghcr.io/baovu2k4/vsf-duocthu-web --set aiService.image.tag="$GITHUB_SHA" --set web.image.tag="$GITHUB_SHA" > /tmp/tagged.yaml
|
|
grep -q "image: \"ghcr.io/baovu2k4/vsf-duocthu-ai-service:$GITHUB_SHA\"" /tmp/tagged.yaml
|
|
grep -q "image: \"ghcr.io/baovu2k4/vsf-duocthu-web:$GITHUB_SHA\"" /tmp/tagged.yaml
|
|
|
|
# These two releases are what realvuxbaro.me actually serves, so their
|
|
# contract is asserted here rather than trusted by review.
|
|
- name: Render the live production manifests
|
|
run: |
|
|
# See the same placeholder note in the previous step -- real secret
|
|
# material never enters Git and this is a render-only dry run.
|
|
helm template medical-chatbot-app infra/helm/medical-chatbot \
|
|
--values infra/helm/medical-chatbot/values-production.yaml \
|
|
--set secret.jwtSecret=ci-render-only-not-a-real-secret \
|
|
> /tmp/prod-app.yaml
|
|
helm template medical-chatbot-data infra/helm/medical-chatbot \
|
|
--values infra/helm/medical-chatbot/values-production-data.yaml \
|
|
> /tmp/prod-data.yaml
|
|
|
|
# A bare `grep -q` fails the step with no indication of which
|
|
# assertion broke, and `set -e` ignores a status inverted with `!`,
|
|
# so a `! grep -q` assertion can never fail at all. Both directions
|
|
# go through helpers that name the pattern and exit explicitly.
|
|
# `--` matters: a YAML list item pattern starts with `-`, which grep
|
|
# would otherwise parse as an option bundle.
|
|
expect() {
|
|
if ! grep -q -- "$2" "$1"; then
|
|
echo "::error::$1 is missing: $2"
|
|
exit 1
|
|
fi
|
|
}
|
|
refute() {
|
|
if grep -q -- "$2" "$1"; then
|
|
echo "::error::$1 must not contain: $2"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Behavioural parity with the audited production runtime contract.
|
|
expect /tmp/prod-app.yaml 'ANSWER_MODEL_ID: "qwen.qwen3-next-80b-a3b"'
|
|
expect /tmp/prod-app.yaml 'ANSWER_PROVIDER: "bedrock-converse"'
|
|
expect /tmp/prod-app.yaml 'EMBEDDING_PROVIDER: "cohere-v4"'
|
|
expect /tmp/prod-app.yaml 'EMBEDDING_DIMENSIONS: "1024"'
|
|
expect /tmp/prod-app.yaml 'EVIDENCE_MINIMUM_SCORE: "0.12"'
|
|
expect /tmp/prod-app.yaml 'RERANK_ENABLED: "true"'
|
|
expect /tmp/prod-app.yaml 'AWS_REGION: "us-east-1"'
|
|
expect /tmp/prod-app.yaml 'checksum/runtime-config:'
|
|
expect /tmp/prod-app.yaml '- host: "readytochat.realvuxbaro.me"'
|
|
|
|
# The production hostname now lives on this cluster, routed and with
|
|
# its own certificate secret -- kept separate from the rehearsal
|
|
# hostname's so one renewal failure cannot take both names offline.
|
|
expect /tmp/prod-app.yaml '- host: "realvuxbaro.me"'
|
|
expect /tmp/prod-app.yaml 'secretName: realvuxbaro-tls'
|
|
expect /tmp/prod-app.yaml 'secretName: readytochat-tls'
|
|
|
|
# Grafana answers on that same public hostname. Anonymous access may
|
|
# be open, but never as Admin, never with the login form disabled,
|
|
# and its root URL must be the name users actually arrive on.
|
|
expect /tmp/prod-app.yaml 'value: "https://realvuxbaro.me/grafana/"'
|
|
refute /tmp/prod-app.yaml 'value: "Admin"'
|
|
|
|
# grep is line-oriented, so read the value on the line after each
|
|
# flag rather than trying to match the pair as one pattern.
|
|
for check in "GF_AUTH_ANONYMOUS_ORG_ROLE:Viewer" "GF_AUTH_DISABLE_LOGIN_FORM:false"; do
|
|
flag=${check%%:*}
|
|
want=${check#*:}
|
|
got=$(grep -A1 -- "$flag" /tmp/prod-app.yaml | grep -- 'value:' | tr -d ' "' | cut -d: -f2)
|
|
if [ "$got" != "$want" ]; then
|
|
echo "::error::$flag rendered as '$got', expected '$want'"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# The app release must own neither data StatefulSet: PostgreSQL and
|
|
# Qdrant belong to the data release, so an app-side sync failure or
|
|
# prune can never delete the corpus or the query history. Only those
|
|
# two use volumeClaimTemplates — the observability PVCs are the app
|
|
# release's own and are expected here.
|
|
refute /tmp/prod-app.yaml 'volumeClaimTemplates'
|
|
expect /tmp/prod-app.yaml 'medical-chatbot-data-medical-chatbot-qdrant'
|
|
|
|
# ...and the data release must own nothing else.
|
|
refute /tmp/prod-data.yaml 'medical-chatbot-data-medical-chatbot-ai-service'
|
|
refute /tmp/prod-data.yaml 'kind: Ingress'
|
|
expect /tmp/prod-data.yaml 'volumeClaimTemplates'
|