diff --git a/README.md b/README.md
index 5b2258d7..38fa2c06 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,8 @@
+
+
Simple to start · platform-grade to grow
@@ -40,9 +42,12 @@ the live cluster.
**Read the docs:** **[platformrelay.github.io/Kollect](https://platformrelay.github.io/Kollect/)** — architecture,
quick start, CR reference, ADRs, and examples. This README is the front door; the site is the map.
-Install paths: **Helm OCI on GHCR** is primary (`oci://ghcr.io/platformrelay/kollect`). **Artifact Hub**
-and **OperatorHub** discoverability are wired at release ([ADR-0708](https://platformrelay.github.io/Kollect/adr/0708-operator-distribution-hubs/));
-use those hubs once listings are live — badge URLs stay out of this README until then.
+Install paths: **Helm OCI on GHCR** is primary (`oci://ghcr.io/platformrelay/kollect`). The chart is
+listed on **[Artifact Hub](https://artifacthub.io/packages/search?repo=kollect)**, and the OLM bundle
+is submitted to the community operator catalogs at release
+([ADR-0708](https://platformrelay.github.io/Kollect/adr/0708-operator-distribution-hubs/)). The
+**[OperatorHub.io](https://operatorhub.io/operator/kollect)** listing goes live when the
+community-operators submission merges upstream.
> **Pre-1.0.** Kollect uses a `v1alpha1` API. Breaking API or default changes may ship in minor
> releases before 1.0; release notes and migration guidance call them out. See the
diff --git a/artifacthub-repo.yml b/artifacthub-repo.yml
index 5f0e027e..1d11f168 100644
--- a/artifacthub-repo.yml
+++ b/artifacthub-repo.yml
@@ -1,13 +1,28 @@
# Artifact Hub repository metadata (ADR-0708), pushed to
# ghcr.io//kollect:artifacthub.io by the release workflow.
#
-# 2026-08-07 — PENDING OPERATOR ACTION: repositoryID below is still the
-# placeholder. Register the chart repository at https://artifacthub.io and paste
-# the real repository ID from the Artifact Hub control panel here; Verified
-# Publisher status only activates once the pushed ID matches the registered one.
-# hack/test/dist_artifacthub_release_test.sh accepts ANY well-formed UUID, so
-# filling in the real value does not turn CI red.
-repositoryID: 00000000-0000-4000-8000-000000000000
+# 2026-08-18 — repository registered on Artifact Hub as `kollect`
+# (https://artifacthub.io/packages/search?repo=kollect); Verified Publisher active.
+#
+# Verified Publisher requires BOTH: the pushed repositoryID matches the registered
+# one, AND the owner email matches the email on the Artifact Hub account that owns
+# the repository (account `konih`). Artifact Hub compares the two addresses directly
+# — it does not send a verification mail.
+repositoryID: cb3be9a6-8e3b-4419-9de5-1184fe349c29
owners:
- - name: kollect contributors
- email: maintainers@platformrelay.github.io
+ - name: Konrad Heimel
+ email: konrad.heimel@gmail.com
+
+# DR-FIND-07: this ONE OCI repository holds two different artifact kinds — the Helm
+# chart at bare semver tags (0.18.0) and the multi-arch controller image at
+# v-prefixed tags (v0.18.0). Artifact Hub treats every semver-ish tag as a chart, so
+# without this it tries to load the image tags as charts and the tracker reports
+# "layer not found" for each one. Bare 0.12.0 is included because that single release
+# published the IMAGE to the chart's bare tag; every other bare tag is a real chart.
+#
+# `version` is a REGULAR EXPRESSION matched against the tag; `name` is an exact match
+# on the package name. Ignored packages are applied the next time the repository is
+# processed, not immediately.
+ignore:
+ - name: kollect
+ version: '^(v[0-9].*|0\.12\.0)$'
diff --git a/config/olm/ci.yaml b/config/olm/ci.yaml
index 13dd941b..e16efb0d 100644
--- a/config/olm/ci.yaml
+++ b/config/olm/ci.yaml
@@ -1,2 +1,8 @@
---
+# NOTE: this file is NOT what gets submitted. hack/operatorhub-pr.sh writes its own
+# copy of ci.yaml into the community-operators PR. Keep the two in sync.
+# `reviewers` takes GitHub usernames (an org does not resolve) — the PR author must
+# appear in the upstream copy for `authorized-changes` / self-merge to apply.
updateGraph: semver-mode
+reviewers:
+ - konih
diff --git a/config/olm/template/manifests/kollect.clusterserviceversion.yaml b/config/olm/template/manifests/kollect.clusterserviceversion.yaml
index 5b0f287e..8036686a 100644
--- a/config/olm/template/manifests/kollect.clusterserviceversion.yaml
+++ b/config/olm/template/manifests/kollect.clusterserviceversion.yaml
@@ -121,11 +121,15 @@ spec:
- base64data: __ICON_BASE64__
mediatype: image/svg+xml
+ # Kollect's controller watches cluster-wide: it never reads olm.targetNamespaces
+ # and sets no WATCH_NAMESPACE, so a namespace-scoped install would silently
+ # collect from the whole cluster. Advertise AllNamespaces only until that
+ # plumbing exists (ADR-0708 follow-up D9).
installModes:
- type: OwnNamespace
- supported: true
+ supported: false
- type: SingleNamespace
- supported: true
+ supported: false
- type: MultiNamespace
supported: false
- type: AllNamespaces
diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md
index 213bbcc0..cccf3947 100644
--- a/docs/getting-started/install.md
+++ b/docs/getting-started/install.md
@@ -64,9 +64,10 @@ for a constrained development environment. The chart does not generate certifica
Helm OCI on GHCR remains the primary install path ([ADR-0705](../adr/0705-release-supply-chain.md)).
Additional distribution wiring is landing under [ADR-0708](../adr/0708-operator-distribution-hubs.md):
-- **Artifact Hub** — once the chart repository is registered and Verified Publisher metadata is
- live, the same OCI chart (`oci://ghcr.io/platformrelay/kollect`) will appear in Artifact Hub
- search. Until listing is confirmed, install from GHCR as above.
+- **Artifact Hub** — the chart repository is registered and listed as
+ [`kollect`](https://artifacthub.io/packages/search?repo=kollect); it indexes the same OCI chart
+ (`oci://ghcr.io/platformrelay/kollect`), so Artifact Hub is a discovery surface, not a separate
+ install path — `helm install` from GHCR exactly as above.
- **OperatorHub / OpenShift** — OLM bundles are generated at release and submitted to the community
operator catalogs when `OPERATORHUB_PAT` is configured. After upstream merge, install via
OperatorHub or the OpenShift console using package **`kollect`**, channel **`stable`**. Until then,
diff --git a/hack/operatorhub-pr.sh b/hack/operatorhub-pr.sh
index d35801f3..961b265a 100755
--- a/hack/operatorhub-pr.sh
+++ b/hack/operatorhub-pr.sh
@@ -8,7 +8,11 @@
# Required env vars:
# VERSION - Release version without 'v' prefix (e.g., 0.17.0)
# IMAGE_DIGEST - Controller image digest (e.g., sha256:abc...)
-# GH_TOKEN - PAT with public_repo scope for fork push and upstream PRs
+# GH_TOKEN - classic PAT for fork push and upstream PRs. Scopes: `public_repo`
+# (clone/push the public forks, open + edit upstream PRs) AND
+# `workflow` — the submission branch is cut from upstream/main, which
+# carries .github/workflows/*, and GitHub rejects a PAT push that
+# introduces workflow files without it once the fork drifts behind.
#
# Optional env vars:
# FORK_OWNER - GitHub org owning the forks (default: platformrelay)
@@ -96,14 +100,27 @@ submit_bundle() {
cp "${CHECKOUT_DIR}/${BUNDLE_DIR}/metadata/"* "${OPERATOR_DIR}/${VERSION}/metadata/"
if [[ -n "${openshift_versions}" ]]; then
- sed -i '/^annotations:/a\ com.redhat.openshift.versions: "'"${openshift_versions}"'"' \
- "${OPERATOR_DIR}/${VERSION}/metadata/annotations.yaml"
+ # Portable insert-after. GNU and BSD/macOS sed disagree on BOTH `-i` (BSD reads the
+ # next argument as the backup suffix) and the one-line `a\text` form, so the previous
+ # `sed -i` worked on the CI runner and died locally with "invalid command code".
+ # awk behaves identically on both, which matters because docs/RELEASE.md and the
+ # workflow's own failure warning both tell operators to re-run this script by hand.
+ local annotations_file="${OPERATOR_DIR}/${VERSION}/metadata/annotations.yaml"
+ local tmp_annotations
+ tmp_annotations="$(mktemp)"
+ awk -v line=" com.redhat.openshift.versions: \"${openshift_versions}\"" \
+ '{ print } /^annotations:/ { print line }' \
+ "${annotations_file}" >"${tmp_annotations}"
+ mv "${tmp_annotations}" "${annotations_file}"
fi
+ # `reviewers` takes GitHub USERNAMES, not orgs. The PR author (the OPERATORHUB_PAT
+ # owner) must appear here on the UPSTREAM default branch for the pipeline to set
+ # `authorized-changes` and self-merge later version bumps; an org never resolves.
cat > "${OPERATOR_DIR}/ci.yaml" <<'CIEOF'
updateGraph: semver-mode
reviewers:
- - platformrelay
+ - konih
CIEOF
git add "${OPERATOR_DIR}/"
@@ -117,33 +134,113 @@ CIEOF
fi
local pr_title="operator ${pr_tag} [CI] kollect (${VERSION})"
+ # Quoted heredoc: no expansion, so markdown backticks are literal and cannot become
+ # command substitution. ${VERSION} is substituted afterwards via the placeholder.
local pr_body
- pr_body="### New Submission
+ pr_body="$(cat <<'PRBODY'
+### New Submission
-**Operator:** kollect
-**Version:** ${VERSION}
+**Operator:** kollect · **Version:** __VERSION__ · **Channel:** stable · **Capability level:** Full Lifecycle
-Update Kollect operator to version ${VERSION}.
+---
+
+## What Kollect does
+
+Kollect turns live Kubernetes state into **durable, queryable inventory**. Operators declare once
+what matters -- via `KollectProfile` (which fields to extract) and `KollectTarget` (which resources
+to watch) -- and Kollect keeps that inventory current, exporting the same canonical snapshot to Git,
+relational databases, object storage, and event streams in parallel.
+
+The motivating problem: everything that wants to know "what is running in this cluster" ends up
+querying the apiserver. That couples every consumer to cluster RBAC, risks watch storms, and pushes
+teams toward storing derived state in etcd, where it does not belong. Kollect inverts this --
+consumers query a **sink**, never the apiserver.
+
+## Features
+
+* **Decoupled read model** -- consumers query a sink, not the apiserver: no RBAC blast radius, no
+ watch-storm risk, no etcd size pressure.
+* **Event-driven collection** -- one shared informer per GVK keeps inventory current as the cluster
+ changes. No polling loops.
+* **Schema-flexible extraction** -- declare the attributes you want with CEL expressions or
+ JSONPath. No bespoke collector per resource kind.
+* **Pluggable sinks** -- snapshot, database, and event sink families fan the same snapshot out to
+ Git, Postgres, object stores, or event streams. No privileged backend tier.
+* **Multi-tenant by design** -- `KollectScope` gates which teams, namespaces, and sinks each tenant
+ may use, so inventory collection can be delegated safely.
+* **Fleet-ready** -- N single-mode operators feed one shared sink, partitioned by `spec.cluster`.
+ There is no central hub tier to operate.
+* **Scale-aware** -- shared informers, export sharding, and tunable reconcile/dispatch concurrency.
+
+## Security
+
+**Runtime hardening.** The manager runs `runAsNonRoot` with a `RuntimeDefault` seccomp profile, a
+read-only root filesystem, `allowPrivilegeEscalation: false`, and **all** Linux capabilities dropped.
-See [release notes](https://github.com/platformrelay/kollect/releases/tag/v${VERSION}) for changes.
+**Least privilege and data handling.**
+
+* Reads only the resources its RBAC allows, with **SubjectAccessReview** checks configurable per
+ target -- collection cannot be used to escalate beyond what the requester may already read.
+* Writes to external sinks using credentials sourced from **`Secret` references only**; credentials
+ never appear in CR specs or logs.
+* Stores **aggregated summaries** in CR `status`, not full resource payloads.
+* Documented guidance to restrict egress with `NetworkPolicy` and require verified TLS to sinks.
+
+**Supply chain.** Every release publishes multi-arch images with **cosign keyless signatures**,
+**SPDX SBOMs**, and **SLSA provenance attestations**, plus `sha256sum` manifests for install YAML
+and the chart tarball. The CSV deployment and `relatedImages` in this bundle are **digest-pinned**,
+not tag-pinned.
+
+**Assurance.** The project runs OpenSSF Scorecard, `govulncheck`, golangci-lint SAST, and dependency
+and license (SCA) policy checks, publishes VEX statements for vulnerability exceptions, and accepts
+private vulnerability reports. See `SECURITY.md` and the published security architecture for trust
+boundaries, tenancy, redaction, and shared-responsibility detail.
+
+## Notes for reviewers
+
+* **Install mode:** `AllNamespaces` only. The controller watches cluster-wide and does not consume
+ `olm.targetNamespaces`, so advertising a namespace-scoped mode would silently collect from the
+ whole cluster. It is deliberately not offered.
+* **Webhooks:** this bundle runs with validating webhooks disabled -- no webhook `Service` or
+ certificate ships in it. CRD schema validation still applies. The Helm chart path offers the full
+ admission stack for users who want it.
+* **Prerequisites:** Kubernetes 1.28+.
+
+Source: https://github.com/platformrelay/kollect ·
+Docs: https://platformrelay.github.io/Kollect/ ·
+Release notes: https://github.com/platformrelay/kollect/releases/tag/v__VERSION__
---
-*This PR was automatically created by the Kollect release workflow.*"
+*Submitted by the Kollect release workflow (`hack/operatorhub-pr.sh`).*
+PRBODY
+)"
+ pr_body="${pr_body//__VERSION__/${VERSION}}"
+ # Look the PR up by branch name and match the owner case-INSENSITIVELY. GitHub stores
+ # the canonical org casing ("PlatformRelay"), so the old `--head "${FORK_OWNER}:${BRANCH}"`
+ # filter silently returned nothing whenever FORK_OWNER differed in case, and the script
+ # then tried to create a duplicate PR and died with "a pull request already exists".
+ local fork_owner_lc
+ fork_owner_lc="$(printf '%s' "${FORK_OWNER}" | tr '[:upper:]' '[:lower:]')"
local existing_pr
existing_pr=$(GH_TOKEN="${GH_TOKEN}" gh pr list \
--repo "${upstream_repo}" \
- --head "${FORK_OWNER}:${BRANCH}" \
+ --head "${BRANCH}" \
--state open \
- --json number \
- --jq '.[0].number // empty' 2>/dev/null || true)
+ --json number,headRepositoryOwner \
+ --jq "[.[] | select((.headRepositoryOwner.login // \"\" | ascii_downcase) == \"${fork_owner_lc}\")] | .[0].number // empty" 2>/dev/null || true)
if [[ -n "${existing_pr}" ]]; then
echo "Updating existing PR #${existing_pr}"
- GH_TOKEN="${GH_TOKEN}" gh pr edit "${existing_pr}" \
- --repo "${upstream_repo}" \
- --title "${pr_title}" \
- --body "${pr_body}"
+ # Use the REST endpoint, NOT `gh pr edit`. The latter resolves assignees/labels/
+ # reviewers via GraphQL and so demands `read:org` ("The 'login' field requires one of
+ # the following scopes: ['read:org']"), which would force a broader PAT than this
+ # script needs. PATCH .../pulls/{n} updates title+body with `public_repo` alone.
+ GH_TOKEN="${GH_TOKEN}" gh api \
+ --method PATCH \
+ "repos/${upstream_repo}/pulls/${existing_pr}" \
+ -f title="${pr_title}" \
+ -f body="${pr_body}" >/dev/null
echo "PR updated: https://github.com/${upstream_repo}/pull/${existing_pr}"
else
echo "Creating new PR..."
diff --git a/hack/test/dist_install_docs_test.sh b/hack/test/dist_install_docs_test.sh
index 9ffa0455..2bfae06b 100755
--- a/hack/test/dist_install_docs_test.sh
+++ b/hack/test/dist_install_docs_test.sh
@@ -25,11 +25,18 @@ for file in "${INSTALL}" "${README}"; do
fail "${file} must mention OperatorHub discoverability"
done
-for file in "${INSTALL}" "${README}"; do
- if grep -E 'artifacthub\.io/badge|operatorhub\.io/operator/kollect|artifacthub\.io/packages/helm' "${file}"; then
- fail "${file} must not ship live hub badge/listing URLs before registration"
- fi
-done
+# ADR-0708 originally forbade ANY hub URL that 404s before that hub lists us. Artifact Hub
+# registration completed 2026-08-18 (repo `kollect`, oci://ghcr.io/platformrelay/kollect,
+# Verified Publisher active), so its badge is unambiguously legitimate.
+#
+# The OperatorHub.io badge ships ahead of the listing by explicit operator decision: the
+# community-operators submission is open and green, and operatorhub.io soft-404s (it serves
+# HTTP 200 with the generic landing page for unknown operators) rather than showing a broken
+# link. Both badges are asserted PRESENT so neither can silently regress.
+grep -Fq 'artifacthub.io/badge/repository/kollect' "${README}" ||
+ fail "${README} must carry the Artifact Hub badge (repository is registered)"
+grep -Fq 'operatorhub.io/operator/kollect' "${README}" ||
+ fail "${README} must carry the OperatorHub.io badge"
pass "install docs describe hub paths without premature listing URLs"
diff --git a/hack/test/dist_operatorhub_pr_test.sh b/hack/test/dist_operatorhub_pr_test.sh
index ecfaa025..cd00c98e 100755
--- a/hack/test/dist_operatorhub_pr_test.sh
+++ b/hack/test/dist_operatorhub_pr_test.sh
@@ -25,6 +25,33 @@ grep -Fq 'redhat-openshift-ecosystem/community-operators-prod' "${SCRIPT}" ||
fail "operatorhub-pr.sh must submit to community-operators-prod"
grep -Fq 'v4.19' "${SCRIPT}" ||
fail "operatorhub-pr.sh must annotate OpenShift v4.19 for prod catalog"
+
+# The three checks below inspect EXECUTABLE lines only — the comments in operatorhub-pr.sh
+# legitimately name the very anti-patterns being banned, and a naive grep matches its own docs.
+CODE="$(grep -v '^[[:space:]]*#' "${SCRIPT}")"
+
+# GNU-only `sed -i