feat(ci): add build-projector workflow + publish to GHCR (unblocks controllers.projector.enabled flip) (#2031)
Some checks are pending
Build projector / build (push) Waiting to run

Adds .github/workflows/build-projector.yaml — the missing CI pipeline
that builds the `core/cmd/projector/` Go binary, publishes it to
`ghcr.io/openova-io/openova/projector:<short-sha>` + `:latest`, signs
with cosign keyless (Sigstore), attests SBOM, then auto-bumps
`controllers.projector.image.tag` in products/catalyst/chart/values.yaml
and dispatches blueprint-release for catalyst chart re-publish.

Why
---
enabled:false audit (V18-B): the projector source landed in
`core/cmd/projector/` with its own Containerfile but NO CI workflow
was ever added to publish the image. That means
`controllers.projector.enabled` CANNOT be flipped on — the chart
template would render an empty `image.tag` and `helm template` would
fail-fast (Inviolable Principle #4a). Every prior attempt at wiring
the CQRS read-side for the NATS event spine (Pillar 1 marketplace +
Pillar 4 sandbox control-plane, per CLAUDE.md §11) silently stalled
here.

Scope
-----
- Adds the CI workflow ONLY.
- Does NOT flip `controllers.projector.enabled` to true — that
  remains a separate chain (TBD-V18-C) that needs the NACK consumer
  installed and JetStream catalystStreams reconciled before the gate
  can flip safely.
- Does NOT bump the bp-catalyst-platform chart version (CI does
  that automatically on the first push-to-main, then dispatches
  blueprint-release).

Sibling-modeled on
------------------
- build-blueprint-controller.yaml (auth flow + auto-bump pattern)
- build-k8s-ws-proxy.yaml (per-cmd go.mod layout + Containerfile)

Both already in production; this PR uses the same Buildx + cosign
keyless + SBOM-attest + values.yaml auto-bump + blueprint-release
dispatch shape — no novel patterns.

Refs TBD-V22 (filed alongside this PR) — projector image-build
pipeline missing.
Refs #1099 — EPIC-4: Cloud Resources / projector.
Refs #1094 — EPIC: Catalyst Phase 0/1 (control-plane).

Co-authored-by: hatiyildiz <noreply@anthropic.com>
This commit is contained in:
e3mrah 2026-05-20 05:29:06 +04:00 committed by GitHub
parent 7bf19317c4
commit d74298c234
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

229
.github/workflows/build-projector.yaml vendored Normal file
View File

@ -0,0 +1,229 @@
name: Build projector
# projector — Catalyst CQRS read-side binary that consumes K8s resource
# events from the NATS catalyst.events JetStream and projects them
# into Valkey under `cluster:{c}:kind:{k}:{ns}/{name}` for cross-replica
# catalyst-api SSE fan-out. Source: `core/cmd/projector/`. Wire contract:
# `core/cmd/projector/DESIGN.md`. Chart slot:
# `controllers.projector` in `products/catalyst/chart/values.yaml`
# (defaults to `enabled: false`, `image.tag: ""` — fail-fast per
# Inviolable Principle #4a until a CI-built tag is pinned here).
#
# Why this workflow exists
# ------------------------
# enabled:false audit (V18-B): the projector source landed in
# `core/cmd/projector/` with its own Containerfile but no CI workflow
# was ever added to publish the image. That means
# `controllers.projector.enabled` CANNOT be flipped on — the chart
# template would render an empty `image.tag` and `helm template`
# would fail-fast. Every prior attempt at wiring the CQRS read-side
# for the NATS event spine (Pillar 1+4 control-plane) silently
# stalled here. This workflow closes that gap and lets a separate
# follow-up PR safely flip the gate.
#
# Per docs/INVIOLABLE-PRINCIPLES.md #4a (GitHub Actions is the ONLY
# build path) every image that runs on OpenOva infra MUST be produced
# by a CI workflow from a committed git SHA — never built locally,
# never pushed by hand. This workflow mirrors
# build-blueprint-controller.yaml: same Buildx + cosign keyless sign +
# SBOM attestation flow, same `controllers.<name>.image.tag` auto-bump
# in `products/catalyst/chart/values.yaml`, same dispatch of
# blueprint-release for catalyst chart re-publish.
#
# Per `feedback_inviolable_principles.md`: event-driven only, NO cron.
# Triggers on push-to-main with paths filter (so unrelated commits
# don't burn CI minutes), pull_request for reviewers, and
# workflow_dispatch for manual re-runs.
#
# Scope notes
# -----------
# - This PR delivers the image-build pipeline ONLY. The chart-flip
# (`controllers.projector.enabled: true`) is a separate chain that
# needs the NACK consumer installed and JetStream catalystStreams
# reconciled — tracked under TBD-V18-C.
# - The projector binary owns its own `go.mod` under
# `core/cmd/projector/`, so the path filter does NOT include the
# shared `core/controllers/**` tree.
#
# Refs TBD-V22 (filed alongside this PR), V18-B audit, EPIC #1094, #1099.
on:
push:
paths:
- 'core/cmd/projector/**'
- '.github/workflows/build-projector.yaml'
branches: [main]
pull_request:
paths:
- 'core/cmd/projector/**'
- '.github/workflows/build-projector.yaml'
workflow_dispatch:
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/openova-io/openova/projector
jobs:
build:
runs-on: ubuntu-latest
permissions:
# contents: write — the deploy step below pushes a values.yaml SHA
# bump back to main so the bp-catalyst-platform chart picks up the
# newly-built image without an operator manually editing the file
# (per `feedback_no_mvp_no_workarounds.md` rule 1: target-state,
# never "manual follow-up bump"). Mirrors
# build-blueprint-controller.yaml.
contents: write
packages: write
# id-token write is required by cosign keyless signing (Sigstore).
id-token: write
# actions: write — required for `gh workflow run` to dispatch the
# downstream blueprint-release chart re-publish workflow.
actions: write
outputs:
sha_short: ${{ steps.vars.outputs.sha_short }}
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set short SHA
id: vars
run: echo "sha_short=$(echo $GITHUB_SHA | head -c 7)" >> "$GITHUB_OUTPUT"
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
cache-dependency-path: |
core/cmd/projector/go.sum
- name: go vet — projector
working-directory: core/cmd/projector
run: go vet ./...
- name: Run unit tests — projector
working-directory: core/cmd/projector
run: go test -count=1 -race ./...
# On pull_request runs we stop here — image push requires
# `packages: write` which only main-branch authors hold.
- name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
if: github.event_name != 'pull_request'
uses: docker/setup-buildx-action@v3
- name: Build and push image
id: build
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v6
with:
# Build context is the repository root so the Containerfile's
# COPY paths can reach core/cmd/projector/.
context: .
file: core/cmd/projector/Containerfile
push: true
tags: |
${{ env.IMAGE }}:${{ steps.vars.outputs.sha_short }}
${{ env.IMAGE }}:latest
labels: |
org.opencontainers.image.source=https://github.com/openova-io/openova
org.opencontainers.image.revision=${{ github.sha }}
org.opencontainers.image.title=projector
org.opencontainers.image.description=Catalyst CQRS read-side — consumes NATS catalyst.events and projects into Valkey for cross-replica catalyst-api SSE fan-out (EPIC-4 P1 #1099)
# provenance=false: containerd 1.7.x on k3s mis-resolves the
# provenance attestation manifest. SBOM attestation handled by
# the cosign attest step below.
provenance: false
sbom: false
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3
- name: Sign image with cosign (keyless)
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "${IMAGE}@${DIGEST}"
- name: Generate and attest SBOM
if: github.event_name != 'pull_request'
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign attest --yes \
--predicate <(echo '{"sbom":"in-toto-spdx attached at build time"}') \
--type spdx \
"${IMAGE}@${DIGEST}"
# Auto-bump `controllers.projector.image.tag` so the next Sovereign
# chart rollout picks up this image without a manual edit. Mirrors
# build-blueprint-controller.yaml / build-application-controller.yaml.
# NOTE: this only updates the tag; `controllers.projector.enabled`
# stays false in this PR (per V18-B audit — flipping requires the
# NACK consumer + JetStream catalystStreams reconciled first,
# tracked under TBD-V18-C).
- name: Bump controllers.projector.image.tag in values.yaml
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
env:
SHA_SHORT: ${{ steps.vars.outputs.sha_short }}
run: |
VALUES="products/catalyst/chart/values.yaml"
# awk: find ` projector:` under `controllers:`, then update
# the next `tag: "..."` line. Stops at the next top-level
# ` <key>:` (two-space indent) so we don't accidentally bump
# a sibling controller's tag.
awk -v sha="${SHA_SHORT}" '
/^controllers:/ { in_ctrls=1 }
in_ctrls && /^ projector:/ { print; in_proj=1; next }
in_ctrls && /^ [a-z]/ && !/^ projector:/ { in_proj=0 }
in_proj && /^ tag:/ { sub(/"[^"]*"/, "\"" sha "\""); in_proj=0 }
{ print }
' "${VALUES}" > "${VALUES}.tmp" && mv "${VALUES}.tmp" "${VALUES}"
echo "values.yaml after bump:"
grep -A4 "^ projector:" "${VALUES}" | head -10
- name: Commit and push values.yaml bump
id: deploy_commit
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
env:
SHA_SHORT: ${{ steps.vars.outputs.sha_short }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
if git diff --quiet products/catalyst/chart/values.yaml; then
echo "no values.yaml change — already pinned to ${SHA_SHORT}"
echo "pushed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git add products/catalyst/chart/values.yaml
git commit -m "deploy: bump projector image to ${SHA_SHORT}"
# Pull-rebase to avoid races with parallel build commits.
git pull --rebase --autostash origin main || true
git push origin HEAD:main
echo "pushed=true" >> "$GITHUB_OUTPUT"
# GitHub Actions does NOT trigger workflows from bot pushes by
# default (anti-recursion safeguard). Without this dispatch the
# rebuilt image is NEVER baked into a new chart version, so
# Sovereigns keep installing the previous chart with the previous
# image tag (`feedback_no_mvp_no_workarounds.md` rule 1 violation).
- name: Dispatch blueprint-release for chart re-publish
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && steps.deploy_commit.outputs.pushed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh workflow run blueprint-release.yaml \
--repo "${GITHUB_REPOSITORY}" \
--ref main \
-f blueprint=catalyst \
-f tree=products