Skip to content

chore(main): release 1.189.0 #1548

chore(main): release 1.189.0

chore(main): release 1.189.0 #1548

Workflow file for this run

name: Quality Gate
# The Quality Gate is the single required check on the PR. Every floor lives
# as a separate job (so failures show up granularly in the Checks UI) and the
# aggregator job at the bottom is what branch protection should require.
#
# Configuration is centralised in .github/quality-gate.yml — thresholds and
# enforcement modes are not hardcoded here.
on:
pull_request:
branches: [main, develop]
types: [opened, synchronize, reopened, labeled, unlabeled, ready_for_review]
concurrency:
group: quality-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
statuses: read
checks: read
env:
GO_VERSION_FILE: go.mod
jobs:
# ---------------------------------------------------------------------
# meta — short job that decides whether to run the gate at all and
# exposes shared values (PR labels, docs-only flag) to downstream jobs.
# ---------------------------------------------------------------------
meta:
name: Meta
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.decide.outputs.skip }}
docs_only: ${{ steps.decide.outputs.docs_only }}
release_please: ${{ steps.decide.outputs.release_please }}
labels: ${{ steps.decide.outputs.labels }}
base_ref: ${{ steps.decide.outputs.base_ref }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install yq + jq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: decide
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_EVENT_PATH: ${{ github.event_path }}
run: |
set -euo pipefail
source scripts/qg/lib.sh
# Skip release-please / bot PRs
actor='${{ github.actor }}'
mapfile -t skip_actors < <(qg_yq '.skip_actors[]?')
skip="false"
for a in "${skip_actors[@]}"; do
if [[ "$actor" == "$a" ]]; then
skip="true"; break
fi
done
# quality-gate-skip nuclear bypass
if qg_has_label quality-gate-skip; then
skip="true"
echo "::warning::Quality Gate bypassed via 'quality-gate-skip' label"
fi
base_ref="origin/${{ github.base_ref }}"
git fetch origin "${{ github.base_ref }}" --quiet || true
# docs-only detection
export QG_BASE_REF="$base_ref"
docs_only="false"
if qg_is_docs_only; then docs_only="true"; fi
# release-please PR detection: branch is deterministic
# ("release-please--branches--<base>"). The PR only bumps versions
# and CHANGELOG, so the heavy floors are noise — we keep
# commit-lint running as a sanity check and skip the rest.
release_please="false"
head_ref='${{ github.head_ref }}'
if [[ "$head_ref" == release-please--* ]]; then
release_please="true"
fi
labels=$(qg_pr_labels | xargs)
{
echo "skip=$skip"
echo "docs_only=$docs_only"
echo "release_please=$release_please"
echo "labels=$labels"
echo "base_ref=$base_ref"
} >> "$GITHUB_OUTPUT"
# ---------------------------------------------------------------------
# Floor 1 — build, vet, gofmt, golangci-lint
#
# Runs the same checks twice: once at the repo root (chatcli main module)
# and once inside operator/ (separate Go module). gofmt is run repo-wide
# because the formatter doesn't care about module boundaries.
# ---------------------------------------------------------------------
build-static:
name: Floor 1 · Build & Static
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- name: go build (root)
run: go build ./...
- name: go build (operator)
working-directory: operator
run: go build ./...
- name: go vet (root)
run: go vet ./...
- name: go vet (operator)
working-directory: operator
run: go vet ./...
- name: gofmt (repo-wide)
run: |
out=$(gofmt -l .)
if [[ -n "$out" ]]; then
echo "::error::gofmt: files not formatted:"
echo "$out"
exit 1
fi
- name: golangci-lint (root, full module)
# Both module baselines are at zero (misspell in PR #1055, the five
# heavy linters in #1056, the remainder — including the operator
# module — in the change that flipped this), so enforcement covers
# the whole tree: pre-existing debt can no longer accumulate
# silently outside PR diffs.
uses: golangci/golangci-lint-action@v6
with:
version: v1.64.8
install-mode: goinstall
args: --timeout=8m
- name: golangci-lint (operator, full module)
uses: golangci/golangci-lint-action@v6
with:
version: v1.64.8
install-mode: goinstall
working-directory: operator
args: --timeout=8m
# ---------------------------------------------------------------------
# Floor 2 — total coverage ratchet (unit tests with -race)
# Also produces coverage.out for floor 3 (patch-coverage).
# ---------------------------------------------------------------------
test-coverage:
name: Floor 2 · Coverage ratchet
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.ratchet.outputs.passed }}
current: ${{ steps.ratchet.outputs.current }}
baseline: ${{ steps.ratchet.outputs.baseline }}
delta: ${{ steps.ratchet.outputs.delta }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- name: go test root + operator (merged profile)
# -coverpkg=./... is REQUIRED for patch-coverage correctness: it
# forces the profile to include every package in the module, even
# those without _test.go files. Without it, brand-new packages
# added in a PR have zero profile entries and look like "100 percent
# covered" to diff-cover. See tools/qg/diffcover/compute.go.
#
# Both modules contribute to a single coverage.out so Floors 2 and
# 3 see the unified picture. Merging by concatenating records
# (with one mode header) is safe: -race forces atomic mode in both
# modules, and tools/qg/diffcover dedupes overlapping ranges.
run: |
go test -race -coverpkg=./... -coverprofile=coverage-root.out ./...
( cd operator && go test -race -coverpkg=./... -coverprofile=../coverage-operator.out ./... )
head -1 coverage-root.out > coverage.out
tail -n +2 coverage-root.out >> coverage.out
tail -n +2 coverage-operator.out >> coverage.out
go tool cover -func=coverage.out | tail -n 1
- name: Upload coverage profile
uses: actions/upload-artifact@v7
with:
name: coverage-out
path: coverage.out
retention-days: 14
- id: ratchet
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_BASE_REF: ${{ github.base_ref }}
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
run: bash scripts/qg/coverage-ratchet.sh coverage.out
# ---------------------------------------------------------------------
# Floor 3 — patch coverage on the diff
# ---------------------------------------------------------------------
patch-coverage:
name: Floor 3 · Patch coverage
needs: [meta, test-coverage]
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.patch.outputs.passed }}
percent: ${{ steps.patch.outputs.percent }}
threshold: ${{ steps.patch.outputs.threshold }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- uses: actions/download-artifact@v6
with:
name: coverage-out
- id: patch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_LABELS: ${{ needs.meta.outputs.labels }}
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
# qg-diffcover is built on demand by scripts/qg/patch-coverage.sh;
# the Go toolchain layer above is the only system dep it needs.
run: bash scripts/qg/patch-coverage.sh coverage.out
# ---------------------------------------------------------------------
# Floor 4 — AI smells scanner
# ---------------------------------------------------------------------
ai-smells:
name: Floor 4 · AI smells
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.docs_only != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.scan.outputs.passed }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: scan
env:
PR_LABELS: ${{ needs.meta.outputs.labels }}
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
run: bash scripts/qg/ai-smells.sh
# ---------------------------------------------------------------------
# Floor 5 — scope budget
# ---------------------------------------------------------------------
scope-budget:
name: Floor 5 · Scope budget
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.scope.outputs.passed }}
files: ${{ steps.scope.outputs.files }}
loc: ${{ steps.scope.outputs.loc }}
code_loc: ${{ steps.scope.outputs.code_loc }}
tooling_loc: ${{ steps.scope.outputs.tooling_loc }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: scope
env:
PR_LABELS: ${{ needs.meta.outputs.labels }}
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
run: bash scripts/qg/scope-budget.sh
# ---------------------------------------------------------------------
# Floor 6 — E2E + race (the big behavioural test)
# ---------------------------------------------------------------------
e2e:
name: Floor 6 · E2E
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.docs_only != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- name: Run e2e suite
run: go test -race -count=1 -timeout 10m ./e2e/...
# ---------------------------------------------------------------------
# Floor 7 — commit hygiene
# ---------------------------------------------------------------------
commit-lint:
name: Floor 7 · Commit lint
needs: meta
if: needs.meta.outputs.skip != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.lint.outputs.passed }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: lint
env:
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
run: bash scripts/qg/commit-lint.sh
# ---------------------------------------------------------------------
# Floor 8 — cyclo on changed files
# ---------------------------------------------------------------------
cyclo-new:
name: Floor 8 · Cyclo (new code)
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.docs_only != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.cyclo.outputs.passed }}
checked: ${{ steps.cyclo.outputs.checked }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: cyclo
env:
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
run: bash scripts/qg/cyclo-new.sh
# ---------------------------------------------------------------------
# Floor 9 — secrets scan (gitleaks on the diff)
# ---------------------------------------------------------------------
secrets-scan:
name: Floor 9 · Secrets scan
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: gitleaks (PR scope)
uses: gitleaks/gitleaks-action@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_ENABLE_COMMENTS: "false"
GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "true"
# ---------------------------------------------------------------------
# Floor 10 — i18n parity (cross-locale keys + Go usage coverage)
# ---------------------------------------------------------------------
i18n-parity:
name: Floor 10 · i18n parity
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.parity.outputs.passed }}
missing_keys: ${{ steps.parity.outputs.missing_keys }}
unknown_usages: ${{ steps.parity.outputs.unknown_usages }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: parity
env:
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
run: bash scripts/qg/i18n-parity.sh
# ---------------------------------------------------------------------
# Floor 11 — CRD drift (controller-gen vs checked-in YAML)
# ---------------------------------------------------------------------
crd-drift:
name: Floor 11 · CRD drift
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.drift.outputs.passed }}
drifted_files: ${{ steps.drift.outputs.drifted_files }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: drift
env:
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
run: bash scripts/qg/crd-drift.sh
# ---------------------------------------------------------------------
# Floor 12 — License headers on new Go files
# ---------------------------------------------------------------------
license-headers:
name: Floor 12 · License headers
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.docs_only != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.headers.outputs.passed }}
missing_count: ${{ steps.headers.outputs.missing_count }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: headers
env:
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
run: bash scripts/qg/license-headers.sh
# ---------------------------------------------------------------------
# Floor 13 — apidiff against base ref
# ---------------------------------------------------------------------
api-breaking:
name: Floor 13 · API breaking changes
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.docs_only != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.apidiff.outputs.passed }}
incompatible_count: ${{ steps.apidiff.outputs.incompatible_count }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: apidiff
env:
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
PR_LABELS: ${{ needs.meta.outputs.labels }}
run: bash scripts/qg/api-breaking.sh
# ---------------------------------------------------------------------
# Floor 14 — binary size budget
# ---------------------------------------------------------------------
binary-size:
name: Floor 14 · Binary size
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.docs_only != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.size.outputs.passed }}
chatcli_bytes: ${{ steps.size.outputs.chatcli_bytes }}
operator_bytes: ${{ steps.size.outputs.operator_bytes }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: size
env:
QG_BASE_REF: ${{ needs.meta.outputs.base_ref }}
run: bash scripts/qg/binary-size.sh
# ---------------------------------------------------------------------
# Floor 15 — LLM provider parity matrix
# ---------------------------------------------------------------------
provider-parity:
name: Floor 15 · Provider parity
needs: meta
if: needs.meta.outputs.skip != 'true' && needs.meta.outputs.release_please != 'true'
runs-on: ubuntu-latest
outputs:
passed: ${{ steps.parity.outputs.passed }}
violations: ${{ steps.parity.outputs.violations }}
providers: ${{ steps.parity.outputs.providers }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- name: Install yq
run: |
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
sudo chmod +x /usr/local/bin/yq
- id: parity
run: bash scripts/qg/provider-parity.sh
# ---------------------------------------------------------------------
# Aggregator — the single status check that branch protection requires.
# Posts a sticky comment with the per-floor verdict.
# ---------------------------------------------------------------------
quality-gate:
name: Quality Gate
needs:
- meta
- build-static
- test-coverage
- patch-coverage
- ai-smells
- scope-budget
- e2e
- commit-lint
- cyclo-new
- secrets-scan
- i18n-parity
- crd-drift
- license-headers
- api-breaking
- binary-size
- provider-parity
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: ${{ env.GO_VERSION_FILE }}
cache: true
- uses: ./.github/actions/setup-qg-tools
with:
with-qg-binaries: 'true'
- name: Render verdict (qg-verdict)
id: verdict
shell: bash
env:
# Floor result statuses.
R_BUILD: ${{ needs.build-static.result }}
R_COV: ${{ needs.test-coverage.result }}
R_PATCH: ${{ needs.patch-coverage.result }}
R_SMELLS: ${{ needs.ai-smells.result }}
R_SCOPE: ${{ needs.scope-budget.result }}
R_E2E: ${{ needs.e2e.result }}
R_LINT: ${{ needs.commit-lint.result }}
R_CYCLO: ${{ needs.cyclo-new.result }}
R_SEC: ${{ needs.secrets-scan.result }}
R_I18N: ${{ needs.i18n-parity.result }}
R_CRD: ${{ needs.crd-drift.result }}
R_LICENSE: ${{ needs.license-headers.result }}
R_APIDIFF: ${{ needs.api-breaking.result }}
R_BINSIZE: ${{ needs.binary-size.result }}
R_PROVPARITY: ${{ needs.provider-parity.result }}
# Floor outputs.
COV_CURRENT: ${{ needs.test-coverage.outputs.current }}
COV_BASELINE: ${{ needs.test-coverage.outputs.baseline }}
COV_DELTA: ${{ needs.test-coverage.outputs.delta }}
PATCH_PCT: ${{ needs.patch-coverage.outputs.percent }}
PATCH_THR: ${{ needs.patch-coverage.outputs.threshold }}
SCOPE_FILES: ${{ needs.scope-budget.outputs.files }}
SCOPE_LOC: ${{ needs.scope-budget.outputs.loc }}
SCOPE_CODE_LOC: ${{ needs.scope-budget.outputs.code_loc }}
SCOPE_TOOL_LOC: ${{ needs.scope-budget.outputs.tooling_loc }}
CYCLO_CHECKED: ${{ needs.cyclo-new.outputs.checked }}
I18N_MISSING: ${{ needs.i18n-parity.outputs.missing_keys }}
I18N_UNKNOWN: ${{ needs.i18n-parity.outputs.unknown_usages }}
CRD_DRIFTED: ${{ needs.crd-drift.outputs.drifted_files }}
LICENSE_MISSING: ${{ needs.license-headers.outputs.missing_count }}
API_INCOMPAT: ${{ needs.api-breaking.outputs.incompatible_count }}
BIN_CHATCLI: ${{ needs.binary-size.outputs.chatcli_bytes }}
BIN_OPERATOR: ${{ needs.binary-size.outputs.operator_bytes }}
PROV_COUNT: ${{ needs.provider-parity.outputs.providers }}
PROV_VIOLATIONS: ${{ needs.provider-parity.outputs.violations }}
# Meta flags.
DOCS_ONLY: ${{ needs.meta.outputs.docs_only }}
RELEASE_PLEASE: ${{ needs.meta.outputs.release_please }}
SKIP: ${{ needs.meta.outputs.skip }}
PR_LABELS: ${{ needs.meta.outputs.labels }}
run: |
set -euo pipefail
# qg-verdict was pre-built by setup-qg-tools; render the body and
# forward the verdict line into $GITHUB_OUTPUT. Body is loaded as
# a heredoc-style multi-line output so the sticky-comment action
# can interpolate it verbatim.
qg-verdict -body body.md >> "$GITHUB_OUTPUT"
{
echo 'body<<__QG_EOF__'
cat body.md
echo '__QG_EOF__'
} >> "$GITHUB_OUTPUT"
- name: Sticky comment on PR
uses: marocchino/sticky-pull-request-comment@v3
with:
header: quality-gate
message: ${{ steps.verdict.outputs.body }}
- name: Final verdict
shell: bash
env:
V: ${{ steps.verdict.outputs.verdict }}
run: |
case "$V" in
pass|bypassed) echo "Quality Gate: $V"; exit 0;;
*) echo "Quality Gate: $V"; exit 1;;
esac