Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a778599
ci: add enforceable quality gates, release train, and docs map
SharedQA Jun 13, 2026
f41be7f
ci: address CodeRabbit review — harden gates, fix injection & masking
SharedQA Jun 13, 2026
0b04854
ci: address CodeRabbit re-review — fork-guard AI job, verify kind, sc…
SharedQA Jun 13, 2026
5a1b1c7
ci(release-train): scope OIDC/attestation permissions to validate-art…
SharedQA Jun 13, 2026
9d92c85
ci(security-gates): fix fetch-depth ternary so nightly sweep is full-…
SharedQA Jun 13, 2026
41f21a9
ci(reviews): document 2-random-reviewer assignment via team code-revi…
SharedQA Jun 13, 2026
eb08419
ci: fix newly-introduced quality-gate failures
SharedQA Jun 15, 2026
569b54e
ci(security-gates): waive 3 unfixable/transitive RUSTSEC advisories i…
SharedQA Jun 15, 2026
6ad5664
ci(security-gates): add .trivyignore for pre-existing HIGH misconfigs
SharedQA Jun 15, 2026
44a8c9b
ci: move advisory AI failure-investigation to its own PR
SharedQA Jun 16, 2026
10d1257
ci: rework #1317 to lean on existing tooling (review: @ktursunov)
SharedQA Jun 16, 2026
2b47fbc
ci(helm): render the umbrella against a CI values file, not bare defa…
SharedQA Jun 16, 2026
6dbe72b
ci: slim `make check` to a fast cluster-free loop; drop the extra ins…
SharedQA Jun 16, 2026
60added
ci(release): render the released chart with the values it requires
SharedQA Jun 16, 2026
e0b1a3b
ci: trim enforce-quality-gates to branch-protection; drop duplicate :…
SharedQA Jun 16, 2026
933bc3c
docs(ci): spell out who runs enforce-quality-gates.sh and when
SharedQA Jun 16, 2026
1ece8c2
ci: run the backend suite once for coverage; fix stale deploy/env notes
SharedQA Jun 16, 2026
a6c8ad4
ci(e2e): drop the separate coverage-e2e job (re-ran the suite, captur…
SharedQA Jun 16, 2026
69c8124
ci(helm): tighten chart-version guard to the formats actually used
SharedQA Jun 16, 2026
2ed56e2
Merge branch 'main' into claude/modest-blackburn-4cc697
SharedQA Jun 22, 2026
5b5f36d
ci: fix helm-validate + docs-gates against repo conventions
Jun 22, 2026
7386691
ci(security-gates): waive unfixable airbyte-cdk transitive CVEs in pi…
Jun 22, 2026
a34333a
ci(helm-validate): complete CI render values (clickhouse/mariadb/redi…
Jun 22, 2026
2e330ab
Merge branch 'main' into claude/modest-blackburn-4cc697
SharedQA Jun 22, 2026
1e77861
ci(helm-validate): fix lint loop exit after helmfile/ removal (#1431)
Jun 22, 2026
b8ea741
Merge remote-tracking branch 'origin/main' into fix-1317
SharedQA Jun 23, 2026
f586bc9
Merge branch 'main' into claude/modest-blackburn-4cc697
SharedQA Jun 23, 2026
d086312
docs: regenerate DOCS_MAP.md after merging main
SharedQA Jun 23, 2026
7dade72
ci: slim #1317 to the release-train + coverage slice (split 4/4)
Jun 23, 2026
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
61 changes: 34 additions & 27 deletions .github/workflows/backend-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ concurrency:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
# Coverage ratchet (was its own workflow; folded in here so all PR-time
# backend checks live together). Floor measured ~58.5% (2026-06-13); set just
# below baseline so it gates today and only ever ratchets upward.
COVERAGE_MIN_LINES: "55"

# Coverage goes to the job summary only (no PR comment), so no write scopes are
# needed — read-only is enough.
Expand All @@ -30,6 +34,7 @@ jobs:
check:
name: Lint & Test
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: src/backend
Expand All @@ -50,42 +55,40 @@ jobs:
with:
workspaces: src/backend -> target

- uses: taiki-e/install-action@375e0c7f08a66b8c2ba7e7eef31a6f91043a81b0 # v2.44.38
with:
tool: cargo-llvm-cov

- name: Check formatting
run: cargo fmt --all -- --check

- name: Clippy
run: cargo clippy --all-targets --all-features

- uses: taiki-e/install-action@cargo-llvm-cov

# cargo-llvm-cov runs the suite under coverage instrumentation (this
# replaces the plain `cargo test`, so tests still run) and emits a single
# Cobertura report.
- name: Run tests with coverage
- name: Validate coverage floor is enforcing
run: |
cargo llvm-cov --no-report --all-features --workspace
cargo llvm-cov report --cobertura --output-path cobertura.xml
[[ "$COVERAGE_MIN_LINES" =~ ^[0-9]+$ ]] || { echo "::error::COVERAGE_MIN_LINES must be numeric"; exit 1; }
(( COVERAGE_MIN_LINES > 0 )) || { echo "::error::COVERAGE_MIN_LINES must be > 0 to gate"; exit 1; }

# Cobertura -> markdown summary (no HTML, no .NET SDK). This is a Docker
# action that runs at the workspace root, so the path is repo-root-relative
# and code-coverage-results.md is written there.
- name: Coverage summary
uses: irongut/CodeCoverageSummary@v1.3.0
with:
filename: src/backend/cobertura.xml
format: markdown
output: both
badge: true
hide_complexity: true
fail_below_min: true # gate: fail the job if overall line rate < 60% (lower threshold)
thresholds: "60 80"

- name: Coverage to job summary
if: always()
- name: Tests + instrumented line coverage (one run, with ratchet)
run: |
f="$GITHUB_WORKSPACE/code-coverage-results.md"
if [ -f "$f" ]; then cat "$f" >> "$GITHUB_STEP_SUMMARY";
else echo "_No coverage summary produced (tests likely failed before coverage ran)._" >> "$GITHUB_STEP_SUMMARY"; fi
# Run the suite ONCE under instrumentation: cargo-llvm-cov runs the
# unit tests (pass/fail) AND measures line coverage in the same pass,
# then fails if coverage drops below the floor. No second test run.
# This supersedes #1429's report-only CodeCoverageSummary for the Rust
# job: same measurement (summary to the job summary) plus a ratcheting
# gate via --fail-under-lines. .NET keeps CodeCoverageSummary below
# (separate tooling). Floor rises toward the 80–85% target; never lowers.
cargo llvm-cov --all-features --workspace \
--lcov --output-path target/coverage-unit.lcov \
--fail-under-lines "$COVERAGE_MIN_LINES"
cargo llvm-cov report --summary-only | tee -a "$GITHUB_STEP_SUMMARY"

- uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: coverage-unit-lcov
path: src/backend/target/coverage-unit.lcov
if-no-files-found: error

dotnet-identity:
name: .NET — Identity Service Build & Test
Expand Down Expand Up @@ -121,6 +124,10 @@ jobs:
--no-build --configuration Release
--collect:"XPlat Code Coverage" --results-directory ./coverage

# .NET line coverage for the identity service is wired separately via the
# coverlet collector in #1329 (different tooling from Rust's cargo-llvm-cov,
# so it can't share this job's command). Once that lands, .NET is measured
# in this job and Rust is ratcheted in `check`.
- name: Integration tests (Testcontainers MariaDB)
run: >
dotnet test tests/Insight.Identity.Tests.Integration/Insight.Identity.Tests.Integration.csproj
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/e2e-bronze-to-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,11 @@ jobs:
- name: Tear down
if: always()
run: ./e2e.sh down || true

# NOTE: instrumented e2e coverage is intentionally NOT a separate job. It would
# re-run the whole suite, and it can't capture anything yet anyway — the rig
# drives prebuilt container images, not instrumented host binaries, so there
# are no profraw files to collect (rig wiring is roadmap 2.15). When that lands,
# coverage gets folded into the `e2e` job above (one instrumented run), not a
# parallel job. Local exploration is still available via `make coverage-e2e`.

269 changes: 269 additions & 0 deletions .github/workflows/release-train.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
# Release train — once-per-day release candidate, manual promotion.
#
# Model: BUILD ONCE, PROMOTE MANY. Every merge already publishes an immutable
# umbrella chart (build-images.yml). This workflow never rebuilds anything:
# it SELECTS a published chart version, validates that exact artifact, and
# (on manual promotion with approval) tags it as the release.
#
# nightly cron → latest published chart becomes the Release Candidate:
# artifact validation + RC prerelease report
# workflow_dispatch → same, for an explicit version (hotfix / off-cycle),
# optionally with promote=true
# promote=true → gated by GitHub Environment `production`
# (QA lead + Release Manager required reviewers);
# on approval: annotated tag v<chart-version> +
# GitHub Release. The TAG IS THE RECORD of the release,
# created BY the pipeline — never the trigger for it.
#
# The team keeps merging to main while an RC is under test: new merges create
# new chart versions and do not disturb the pinned RC. "Feature freeze" =
# freezing WHICH VERSION gets promoted, not freezing the branch.

name: Release Train

on:
schedule:
- cron: "0 3 * * *" # nightly RC cut
workflow_dispatch:
inputs:
chart_version:
description: "Umbrella chart version to validate (empty = latest published)"
required: false
type: string
promote:
description: "Promote to release (requires production environment approval)"
required: false
type: boolean
default: false

# Least privilege at the top; jobs elevate only the scope they actually need
# (validate-artifact → OIDC/attestations; rc-report + promote → contents: write).
permissions:
contents: read
packages: read # resolve chart versions from GHCR (resolve job)

concurrency:
group: release-train
cancel-in-progress: false

env:
CHART_OCI: oci://ghcr.io/constructorfabric/charts/insight
CHART_PKG: charts%2Finsight
KUBECONFORM_VERSION: v0.6.7
KUBECONFORM_SHA256: 95f14e87aa28c09d5941f11bd024c1d02fdc0303ccaa23f61cef67bc92619d73

jobs:
resolve:
name: resolve-rc-version
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
version: ${{ steps.pick.outputs.version }}
steps:
- name: Pick chart version (input or latest published)
id: pick
# INPUT_VERSION via env (never interpolated into the script) + strict
# semver validation — blocks shell injection from a dispatch input.
env:
GH_TOKEN: ${{ github.token }}
INPUT_VERSION: ${{ inputs.chart_version }}
run: |
set -euo pipefail
if [[ -n "${INPUT_VERSION:-}" ]]; then
if [[ ! "$INPUT_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::invalid chart_version '$INPUT_VERSION' (expected semver X.Y.Z)"; exit 1
fi
VERSION="$INPUT_VERSION"
else
VERSION=$(gh api "orgs/constructorfabric/packages/container/${CHART_PKG}/versions?per_page=100" \
--jq '.[].metadata.container.tags[]' \
| grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
fi
[[ -n "$VERSION" ]] || { echo "::error::could not resolve a chart version"; exit 1; }
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Release candidate: insight chart $VERSION"

validate-artifact:
name: validate-artifact
needs: resolve
runs-on: ubuntu-latest
timeout-minutes: 30
# OIDC + attestation scopes live only here — the only job that verifies SLSA.
# (Job-level permissions default every unlisted scope to none, so packages
# is re-declared for the registry login below.)
permissions:
packages: read # helm/docker registry login
id-token: write # OIDC for SLSA provenance verification
attestations: read # verify SLSA provenance
steps:
- uses: azure/setup-helm@fe7b79cd5ee1e45176fcad797de68ecaf3ca4814 # v4.2.0
with:
version: v3.14.0

- name: Login registries
env:
GH_TOKEN: ${{ github.token }}
run: |
echo "$GH_TOKEN" | helm registry login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
echo "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin

- name: Pull the EXACT artifact under release
run: helm pull "$CHART_OCI" --version "${{ needs.resolve.outputs.version }}"

- name: Verify SLSA provenance of the chart artifact
env:
GH_TOKEN: ${{ github.token }}
run: |
gh attestation verify "oci://ghcr.io/constructorfabric/charts/insight:${{ needs.resolve.outputs.version }}" \
--owner constructorfabric

- name: Render the released chart with required values
run: |
set -euo pipefail
V="${{ needs.resolve.outputs.version }}"
# The umbrella fail-fasts without required inputs (infra image tags,
# DB names, tenant) — by design, so a bare render can't succeed. Supply
# them here; the SERVICE image tags are pinned to the release version V
# so the image-existence check below verifies the ACTUAL published
# images. (PR-time render uses charts/insight/ci/ci-values.yaml.)
helm template insight "insight-$V.tgz" \
--namespace insight \
--set global.tenantDefaultId=11111111-1111-1111-1111-111111111111 \
--set ingestion.reconcile.tenantId=rc-validate \
--set clickhouse.database=insight \
--set clickhouse.username=default \
--set clickhouse.image.tag=25.3 \
--set mariadb.database=identity \
--set mariadb.username=identity \
--set mariadb.auth.username=identity \
--set mariadb.auth.database=identity \
--set apiGateway.authDisabled=true \
--set apiGateway.image.tag="$V" \
--set analyticsApi.image.tag="$V" \
--set identity.image.tag="$V" \
--set frontend.image.tag="$V" \
> rendered.yaml
echo "rendered $(grep -c '^kind:' rendered.yaml) objects"

- name: kubeconform rendered manifests
run: |
set -euo pipefail
# Verified download: fetch to disk, check SHA256, then extract & run.
curl -sSL -o kubeconform.tgz \
"https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz"
echo "${KUBECONFORM_SHA256} kubeconform.tgz" | sha256sum -c -
tar xzf kubeconform.tgz kubeconform
./kubeconform -strict -ignore-missing-schemas -summary rendered.yaml
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Release artifact integrity — every referenced image must exist in the registry
run: |
set -euo pipefail
# The :latest / floating-tag gate lives in helm-validate.yml (it scans
# the source values.yaml). Here we do the part nothing else does:
# verify each image in the RENDERED released artifact is actually
# published. The render pins service tags to the release version, so a
# floating tag can't reach this step anyway.
rc=0
mapfile -t images < <(grep -E '^\s+image:\s' rendered.yaml | sed -E 's/.*image:\s*"?([^"]+)"?\s*$/\1/' | sort -u)
echo "checking ${#images[@]} unique image refs"
for img in "${images[@]}"; do
if docker manifest inspect "$img" >/dev/null 2>&1; then
echo " ✓ $img"
else
echo "::error::image referenced by chart does not exist: $img"; rc=1
fi
done
exit $rc

rc-report:
name: rc-report
needs: [resolve, validate-artifact]
runs-on: ubuntu-latest
timeout-minutes: 5
# Creates/edits the RC prerelease → needs write (resolve & validate are read-only).
permissions:
contents: write
steps:
- name: Publish / refresh RC prerelease record
env:
GH_TOKEN: ${{ github.token }}
V: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
TAG="rc-${V}"
NOTES=$(cat <<EOF
## Release Candidate — insight chart \`${V}\`

Cut by release-train run [${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) on $(date -u +%F).

Artifact validation (the exact OCI artifact, no rebuild):
- ✅ SLSA provenance verified
- ✅ helm template renders with production-shaped values
- ✅ kubeconform manifest validation
- ✅ all referenced images exist; no \`:latest\` / floating tags

Pending environment gates before promotion (see Target Operating Model §4/§9):
- QA regression pack on QA environment
- Golden-dataset data regression N−1 → N
- Latest nightly fresh-install green

Promote via: Actions → Release Train → Run workflow → chart_version=\`${V}\`, promote=\`true\`.
EOF
)
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --notes "$NOTES"
else
gh release create "$TAG" --repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" --prerelease \
--title "RC: insight chart ${V}" --notes "$NOTES"
fi

promote:
name: promote-to-release
# Manual dispatch only, never the cron — and only after artifact validation.
if: ${{ github.event_name == 'workflow_dispatch' && inputs.promote }}
needs: [resolve, validate-artifact]
runs-on: ubuntu-latest
timeout-minutes: 10
# THE GATE: required reviewers (QA lead + Release Manager) configured by
# scripts/ci/enforce-quality-gates.sh. This job PAUSES until both approve.
# The recorded approval is the signed release checklist.
environment: production
# Elevated write scoped to this job only — it creates the tag + release.
permissions:
contents: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: Create annotated release tag (the immutable release record)
env:
V: ${{ needs.resolve.outputs.version }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# persist-credentials:false → authenticate the push explicitly.
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git config user.name "release-train"
git config user.email "release-train@users.noreply.github.com"
git tag -a "v${V}" -m "Release: insight umbrella chart ${V} (approved via production environment, run ${{ github.run_id }})"
git push origin "v${V}"

- name: Finalize GitHub Release
env:
GH_TOKEN: ${{ github.token }}
V: ${{ needs.resolve.outputs.version }}
run: |
set -euo pipefail
gh release delete "rc-${V}" --repo "$GITHUB_REPOSITORY" --yes 2>/dev/null || true
gh release create "v${V}" --repo "$GITHUB_REPOSITORY" \
--title "insight ${V}" \
--notes "Released umbrella chart \`${V}\` (oci://ghcr.io/constructorfabric/charts/insight:${V}).

Deploy: clusters are deployed from the private \`infra/insight-gitops\` repo (Makefile-driven, OCI-pinned umbrella chart — see docs/components/deployment/gitops/README.md). Bump its pinned umbrella version to \`${V}\`."

- name: Promotion summary
run: |
echo "## ✅ insight ${{ needs.resolve.outputs.version }} promoted" >> "$GITHUB_STEP_SUMMARY"
echo "Next: deploy via the private infra/insight-gitops repo (bump the pinned umbrella version)." >> "$GITHUB_STEP_SUMMARY"
Loading
Loading