✨ feat(dist): hub distribution — OperatorHub + Artifact Hub (ADR-0708) - #284
Conversation
There was a problem hiding this comment.
Pull request overview
Implements ADR-0708’s “hub distribution” lane so Kollect remains primarily installable via Helm OCI on GHCR while also becoming discoverable via Artifact Hub and distributable via OperatorHub/OLM through release automation and CI meta-tests.
Changes:
- Adds Artifact Hub metadata plumbing (chart annotations + release-time
oras pushofartifacthub-repo.yml). - Introduces OLM bundle templating + a
make generate-olm-bundlegenerator, plus automated dual-upstream OperatorHub PR creation in the release workflow. - Adds ADR/docs updates and CI/doc verification meta-tests to gate the distribution wiring.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Mentions the new hub distribution paths while keeping GHCR Helm OCI as primary. |
| Makefile | Adds generate-olm-bundle target to render a registry+v1 OLM bundle from templates + CRDs. |
| hack/test/dist_operatorhub_pr_test.sh | Meta-test to validate OperatorHub PR automation supports dual upstreams and DRY_RUN. |
| hack/test/dist_olm_bundle_test.sh | Meta-test to validate generate-olm-bundle output completeness and expected CSV content. |
| hack/test/dist_install_docs_test.sh | Meta-test ensuring docs mention hubs but avoid premature live badge/listing URLs. |
| hack/test/dist_ci_wiring_test.sh | Meta-test ensuring dist_* scripts run in CI before the generic lint step. |
| hack/test/dist_artifacthub_release_test.sh | Meta-test validating release workflow pushes Artifact Hub metadata and preserves DR-FIND-07 guard. |
| hack/test/dist_artifacthub_chart_test.sh | Meta-test validating required Artifact Hub annotations and CRD listing in Chart.yaml. |
| hack/test/dist_adr_0708_indexed_test.sh | Meta-test verifying ADR-0708 exists and is indexed under theme 07. |
| hack/operatorhub-pr.sh | New script to generate/verify bundles and open/update PRs in OperatorHub upstream repos. |
| hack/docs/verify.sh | Wires ADR/docs distribution checks into docs verification. |
| docs/getting-started/install.md | Documents discoverability paths and clarifies hub listings/badges are omitted until live. |
| docs/adr/README.md | Indexes ADR-0708 under theme 07. |
| docs/adr/0708-operator-distribution-hubs.md | Adds ADR defining the Artifact Hub + OperatorHub distribution approach and constraints. |
| config/olm/template/metadata/annotations.yaml | Adds OLM bundle annotations template (package/channels/mediatype). |
| config/olm/template/manifests/kollect.clusterserviceversion.yaml | Adds CSV template including RBAC, deployment, owned CRDs, and digest-pinned image placeholders. |
| config/olm/ci.yaml | Adds updateGraph config used by OperatorHub repos. |
| charts/kollect/Chart.yaml | Adds Artifact Hub operator metadata annotations (CRDs/links/images/etc.). |
| artifacthub-repo.yml | Adds Artifact Hub repo metadata file (placeholder repositoryID). |
| .github/workflows/release.yaml | Pushes Artifact Hub metadata via oras; adds OperatorHub PR job using generated OLM bundle. |
| .github/workflows/ci.yaml | Runs hub distribution meta-tests (hack/test/dist_*_test.sh) before lint. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| echo "Generating OLM bundle for version $$VERSION (digest: $$IMAGE_DIGEST)..." && \ | ||
| mkdir -p "$$BUNDLE_DIR/manifests" "$$BUNDLE_DIR/metadata" && \ | ||
| ICON_B64=$$(base64 < docs/assets/logo.svg | tr -d '\n') && \ | ||
| sed "s/__VERSION__/$$VERSION/g; s/__DATE__/$$DATE/g; s/__ICON_BASE64__/$$ICON_B64/g; s|__IMAGE_DIGEST__|$$IMAGE_DIGEST|g" \ |
| printf 'docs verify: all available checks passed\n' | ||
| bash hack/test/dist_adr_0708_indexed_test.sh | ||
| bash hack/test/dist_install_docs_test.sh |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
REVIEW — Kollect/dist-hub-distribution @
|
| Gate | Result | Notes |
|---|---|---|
preflight / test / kind-smoke / Analyze (Go) |
✅ | trusted per lane report (required checks green) |
| actionlint (release.yaml) | ✅ | trusted; only pre-existing SC2129 note at ci.yaml:193 repo-wide |
| SonarCloud quality gate (PR 284) | ✅ | re-queried live: projectStatus.status = OK; new_security_rating = 1. S7636 fix in 1e635e9 verified — no ${{ secrets.* }} remains inside any run: block |
| shellcheck (new scripts) | ✅ | only SC2016 info on an intentional single-quoted grep pattern |
| Action pin verification | ✅ | oras-project/setup-oras@1d808f7d… is a real commit == tags v2/v2.0/v2.0.1, and its releases.json covers oras 1.3.2. All uses: are full 40-hex SHAs; no pull_request_target |
| OLM bundle generation (reproduced out-of-tree) | ✅ | valid YAML; kollect.v<version>; 11 owned CRDs == 11 bundle CRD files; alm-examples fields all exist in the real CRD schemas; icon base64 round-trips; no leftover __PLACEHOLDER__ |
CSV RBAC vs config/rbac/role.yaml |
✅ (today) | byte-for-byte identical — but ungated, see D5 |
operator-sdk bundle validate |
not available locally and not run anywhere in CI — see D11 | |
e2e-multitenant |
known-flaky on main; not attributed to this PR | |
| Multi-part stub/TODO rule | ✅ | diff is 1163 insertions / 0 deletions; no TODO/FIXME/stub marker removed or added |
| ID | Sev | Area | Finding | Evidence | Blocks? |
|---|---|---|---|---|---|
| D1 | P1 | Release robustness | Artifact Hub oras steps run in the critical release path with no continue-on-error, before Publish GitHub Release. An oras-download / login / push failure aborts the job after images + chart are pushed and cosign-signed, leaving a tag with GHCR artifacts but no GitHub Release and no assets (install.yaml, SBOMs, checksums, provenance, sigstore bundles). Contradicts ADR-0708's own consequence: "Soft-fail hub jobs preserve tag-release success". |
.github/workflows/release.yaml:365-386 vs :416 |
Yes |
| D2 | P1 | Correctness / docs | artifacthub.io/images hardcodes ghcr.io/platformrelay/kollect:v0.17.0 with no sync gate and no runbook step. docs/RELEASE.md §3 tells the maintainer to bump only version/appVersion and was not updated; the new test only greps the repo prefix, so it passes forever. From v0.18.0 Artifact Hub advertises — and security-scans — the wrong image. An explicit artifacthub.io/images list overrides AH's automatic extraction, so this is worse than omitting it. |
charts/kollect/Chart.yaml:84-86; hack/test/dist_artifacthub_chart_test.sh:43-45; docs/RELEASE.md:169-178 |
Yes |
| D3 | P2 | Security | operatorhub-pr carries secrets.OPERATORHUB_PAT (cross-repo write: fork push + PRs to two third-party repos) but declares no environment:. The release job documents the model at :92-95 — the protected release environment is what stops write creds being obtained by stripping the eligibility job. gh secret list --env release is empty, so the PAT will land repo-level and be readable by any job on any branch with no approval gate. Not yet live (secret unset today) → P2. |
.github/workflows/release.yaml:434-441, :457 |
No |
| D4 | P2 | Security | operatorhub-pr checks out ref: ${{ needs.release.outputs.tag }} — a mutable tag — and re-verifies nothing, while the release job explicitly refuses to (ref: needs.eligibility.outputs.sha + the TAG_SHA != EXPECT_SHA guard). The code executed with the PAT in env and the bundle submitted upstream come from whatever the tag points at. Mitigated by the protect-release-tags ruleset (create/update/delete restricted, admin bypass only) → P2. Fix: needs: [eligibility, release] + ref: ${{ needs.eligibility.outputs.sha }}. |
.github/workflows/release.yaml:438, :445 |
No |
| D5 | P2 | Drift gate | CSV clusterPermissions is a hand copy of the controller-gen-generated config/rbac/role.yaml (identical today — verified). task verify gates role.yaml, but nothing compares the CSV copy. The next +kubebuilder:rbac change silently ships an under-privileged bundle that installs fine then 403s at runtime. ADR-0708 itself flags "Hand CSV drift risk". |
config/olm/template/manifests/kollect.clusterserviceversion.yaml:137-237 |
No |
| D6 | P2 | Test | Inverted gate: the meta-test requires the placeholder repositoryID 00000000-…, so CI goes red the moment the real Artifact Hub ID is filled in. The test blocks its own remediation. Assert a well-formed UUID instead. |
hack/test/dist_artifacthub_release_test.sh:25-27 |
No |
| D7 | P2 | Correctness | maintainers@platformrelay.github.io is fabricated (new in this PR; absent from main). A github.io Pages host cannot receive mail. Artifact Hub matches owners[].email against a registered AH user for the ownership claim, so Verified Publisher can never succeed as written; community-operators also expects a reachable CSV maintainer. The ADR documents the placeholder repositoryID but not this. |
config/olm/template/manifests/kollect.clusterserviceversion.yaml:109; artifacthub-repo.yml:3-4 |
No |
| D8 | P2 | Consistency | config/olm/ci.yaml is dead — nothing reads it; the script writes its own heredoc operators/kollect/ci.yaml that additionally sets reviewers: [platformrelay]. Two sources of truth. Also reviewers upstream is a list of GitHub usernames whose approvals CI recognises; platformrelay is an org, so authorization likely won't resolve. |
config/olm/ci.yaml; hack/operatorhub-pr.sh:103-107 |
No |
| D9 | P2 | Correctness | installModes advertises OwnNamespace/SingleNamespace: true, but the CSV deployment never consumes olm.targetNamespaces (no WATCH_NAMESPACE downward API, no scoping arg) and grants clusterPermissions. A single-namespace install silently collects cluster-wide — beyond the scope the user selected. |
…clusterserviceversion.yaml:124-132 vs :272-338 |
No |
| D10 | P3 | Fragility | __ICON_BASE64__ is substituted with a /-delimited sed while the digest on the same line correctly uses |. base64 legitimately contains /; today's logo happens to encode with zero slashes (verified) so it works, but any logo change breaks generation — and in the release path that failure is swallowed by continue-on-error. |
Makefile:249-250 |
No |
| D11 | P3 | Test depth | The bundle gate is grep-only: no YAML/CSV schema parse, no operator-sdk bundle validate --select-optional suite=operatorframework (what upstream CI runs). Assertions are real, not tautological — just shallow for a hand-templated bundle. |
hack/test/dist_olm_bundle_test.sh |
No |
| D12 | P3 | Portability | for d in "${CLEANUP_DIRS[@]}" under set -u with an empty array is unbound on bash 3.2 (macOS), so the EXIT trap aborts a local DRY_RUN=1 run. CI (bash 5) unaffected. |
hack/operatorhub-pr.sh:31-33 |
No |
Highest-risk area — outbound third-party PRs (a508caf): what holds up
- Trigger surface is tight.
if: … && github.event_name == 'push'means aworkflow_dispatchrebuild (the documented "rebuild assets for testing" path, which can also mark drafts/pre-releases) can not open upstream PRs.needs: releasechains back througheligibility, andprotect-release-tagsrestricts tag creation to admins. - Idempotent. Branch
kollect-v<version>is force-pushed and an existing open PR is updated rather than duplicated (gh pr list --head→gh pr edit), so a workflow re-run or repeated release does not spam upstream. - No untrusted-input expansion in the PAT-bearing path.
VERSIONtraces back to the regex-validated tag in the eligibility job; the PR title/body contain no attacker-influenceable content; nothing usespull_request_target; everyuses:is SHA-pinned (verified against the upstream repos). - Residual gaps: D3 (no environment gate on the PAT) and D4 (mutable-tag checkout of the very code that runs with the PAT).
- Failure mode is fully silent.
continue-on-error: trueplus theif [[ -z "$GH_TOKEN" ]] … exit 0skip means a missing fork, an expired PAT, an upstream rename, or a bundle error all render as a green job. That is the intended "soft-fail", but there is no notification path — consider at minimum a::warning::annotation and a step summary line so a failed submission is visible without reading logs.
Functional correctness: ADR-0708's contract is met item by item — (1) Helm OCI unchanged as primary; (2) Chart annotations complete, artifacthub-repo.yml pushed via oras to the chart OCI repo after helm push + cosign, DR-FIND-07 guard retained and ordered correctly; (3) make generate-olm-bundle produces package kollect / channel stable, digest-pinned to the image digest (not the chart digest) in both the deployment and relatedImages; (4) CSV RBAC taken from the real config/rbac/role.yaml; (5) script + soft-fail job with v4.19 OpenShift annotation and platformrelay fork default; (6) skipped items honoured. --validating-webhooks-enabled=false is a real flag (cmd/startup_flags.go:63), not hallucinated. Docs are honest — no fabricated badges, and the guard test enforces that. Failing/at-risk criteria: the AH metadata push is not soft-fail (D1, contradicting the ADR), and the AH image annotation is stale-by-construction after the next bump (D2).
Coverage delta: n/a — no Go production code changed. Shell/YAML meta-tests: 7 new hack/test/dist_*_test.sh scripts, all wired into the CI lint job before task lint (wiring itself locked by dist_ci_wiring_test.sh), two also into hack/docs/verify.sh. Assertions are substantive, not tautological — with the exceptions noted in D6 (inverted) and D11/D2 (too shallow to catch the invariants that matter).
Summary / next steps — required before merge:
- D1 — make the Artifact Hub oras steps soft-fail and/or move them after
Publish GitHub Release, so hub metadata can never abort a release mid-publish. - D2 — gate
artifacthub.io/imagesagainstappVersionindist_artifacthub_chart_test.shand add it todocs/RELEASE.md§3.
Strongly recommended in the same pass (cheap, and they protect the credential and the bundle): D3 (environment-scope the PAT), D4 (check out the eligibility SHA), D5 (CSV↔role.yaml drift gate), D6 (un-invert the repositoryID assertion). D7–D12 are notes.
Independent review of the diff only; I did not author any of this code and I am not authorising a merge.
REVIEW — Kollect/dist/hub-distribution @
|
| Gate | Result | Notes |
|---|---|---|
PR check rollup @ 264a01d5 |
✅ | 36/36 SUCCESS (preflight, test, kind-smoke, Analyze (Go), lint, verify, helm, vulncheck, CodeQL, gitleaks) |
e2e-multitenant |
✅ | SUCCESS at this head — the prior advisory failure no longer applies |
| SonarCloud quality gate | ✅ | OK, all 5 conditions pass, 0.0% new duplication, 100% hotspots reviewed |
mergeStateStatus |
✅ | CLEAN / MERGEABLE |
hack/test/dist_*_test.sh (7) |
✅ | all pass in the worktree; wired into CI lint before task lint, with yq installed earlier in the same job (fail-closed if absent) |
| Go gate matrix | n/a | no Go production code in the diff |
| Worktree hygiene | ✅ | git status --porcelain = 0; generated dist/olm-bundle/ is gitignored (.gitignore:42) |
The six prior findings
| ID | Status | Evidence |
|---|---|---|
| D1 (P1) | ✅ fixed | The three hub steps are now the last steps of release (.github/workflows/release.yaml:415/422/446), all after Publish GitHub Release (:391), all continue-on-error: true — reporter included, so it cannot reintroduce the abort. Reporters read steps.<id>.outcome, correctly, since continue-on-error pins .conclusion to success. Push gates on steps.setup-oras.outcome == 'success'; that replaces the implicit success() guard but stays safe because setup-oras is skipped on any earlier core failure. Nothing after publish can flip the job result, so needs.release.result == 'success' and operatorhub-pr are unaffected. |
| D2 (P1) | ✅ fixed | dist_artifacthub_chart_test.sh:46-77 yq-parses appVersion and asserts the annotation tag equals v<appVersion>, with non-empty guards on both sides and a found_controller_image guard. docs/RELEASE.md:177-192 adds the bump to step 3 with the "explicit list overrides auto-extraction" rationale. |
| D3 (P2) | ✅ fixed | environment: release at :484, gated by dist_operatorhub_pr_test.sh:56-58. Side effect is real — see P2-1. |
| D4 (P2) | ✅ fixed | needs: [eligibility, release] (:476) + ref: ${{ needs.eligibility.outputs.sha }} (:492), matching the release job's own SHA binding. |
| D5 (P2) | ✅ fixed | dist_olm_bundle_test.sh:48-79 — order- and key-order-insensitive normalisation of both sides, diffed, with explicit non-empty guards on both so a mistyped path fails loudly. Reads the generated bundle CSV, so it also covers the generator. |
| D6 (P2) | ✅ fixed | dist_artifacthub_release_test.sh:20-33 asserts a well-formed UUID (placeholder and real ID both pass — the gate no longer blocks its own remediation). |
ADR-0708 integrity — the highest-risk change in the diff
Legitimate reconciliation, not goalpost-moving. git diff 1e635e9ce..264a01d5a -- docs/adr/0708-operator-distribution-hubs.md is +11/−3, and all three deleted lines are re-emitted text (items 4 and 5 reflowed to gain a clause; old item 6 Skipped: renumbered verbatim to item 7). Every edit adds an obligation:
- item 4 now requires the CSV↔
role.yamldrift gate; - item 5 now requires the protected
releaseenvironment and the eligibility-proven SHA; - a new item 6 requires that hub steps run after
Publish GitHub Releaseand that soft-fail be visible viasteps.<id>.outcome.
Decisively: the Consequences line D1 was measured against — "Soft-fail hub jobs preserve tag-release success when PAT/forks are absent" (:88) — is untouched, and the workflow now actually satisfies it. The ADR was tightened around the fix, not loosened around the bug. I also grepped for citations of ADR-0708 by item number; the 6→7 renumber breaks nothing (only file-level links exist).
Non-vacuity — 12 mutations, all in temp copies (worktree untouched)
| Mutation | Expected | Result |
|---|---|---|
D2: bump appVersion 0.17.0→0.18.0, annotation untouched |
red | ✅ red, exact message |
D2: annotation tag →v0.16.0, appVersion untouched |
red | ✅ red |
D5: delete one - watch verb from CSV |
red | ✅ red, rule-level diff |
D5: add nodes to a CSV resource list |
red | ✅ red |
D5: append a bogus rule to role.yaml only |
red | ✅ red |
D5: rename CSV serviceAccountName |
anti-vacuity guard | ✅ "would pass vacuously" |
D6: repositoryID: not-a-uuid |
red | ✅ red |
| D6: plausible real UUID | green | ✅ green (gate doesn't block remediation) |
D1: move Set up oras before the publish |
red | ✅ red, with indices |
D1: strip continue-on-error from the reporter |
red | ✅ red |
D3: remove environment: |
red | ✅ red |
D4: revert ref to needs.release.outputs.tag / drop eligibility from needs |
red | ✅ red (both) |
Controls green in every case. No gate in this diff passes vacuously.
Findings
| ID | Sev | Area | Finding | Evidence | Blocks? |
|---|---|---|---|---|---|
| P2-1 | P2 | Docs / ops | The environment: release gate's operator-visible cost is documented only in a YAML comment and the ADR, never in the runbook. Verified live: the release env carries a required_reviewers rule, so every release now stops for a second manual approval after the release job already succeeded; timeout-minutes: 15 does not cover the waiting-for-approval state, so the run sits open until a human acts; rejecting it marks the terminal job failed, so a successfully published release shows a red run; and OPERATORHUB_PAT is in neither repo nor release env secrets, so post-approval the step self-skips — today the prompt buys a no-op. A dedicated environment is not the fix: the v*.*.* tag policy alone stops nothing an attacker's tagged workflow copy couldn't match, so the required-reviewers rule is the entire security value and dropping it would be strictly weaker. One paragraph in docs/RELEASE.md §"Cut a release" + "PAT must be a release environment secret". |
docs/RELEASE.md:203-236 vs .github/workflows/release.yaml:478-484 |
No |
| P2-2 | P2 | Tests | CSV spec.install.spec.permissions (namespaced leader-election rules) still an ungated hand copy of config/rbac/leader_election_role.yaml. I diffed by hand: currently in sync, so latent risk not live bug. Honestly flagged as deferred, but the RULE_NORMALIZE machinery landed for D5 applies verbatim — ~6 lines to close. |
config/olm/.../kollect.clusterserviceversion.yaml:238; hack/test/dist_olm_bundle_test.sh:57 |
No |
| P3-1 | P3 | Tests | If repositoryID: is absent entirely, the grep | head | sed pipeline fails inside $( ) and set -euo pipefail aborts before the [[ -n ]] guard can print. Verified: exit 1, zero diagnostic. Fail-closed, but mute. Fix: | head -1 || true. |
hack/test/dist_artifacthub_release_test.sh:26 |
No |
| P3-2 | P3 | Tests | Step-graph gate asserts publish < oras/push/report but never push < report. A reorder would leave the reporter reading an empty outcome and reporting success on an unpublished push. |
hack/test/dist_artifacthub_release_test.sh:89-94 |
No |
| P3-3 | P3 | CI | The release job has no timeout-minutes (only operatorhub-pr does). Pre-existing, but this PR moved two unbounded network steps to the job's tail, so a hung oras push now holds the runner for the 6h default after the Release is published, with operatorhub-pr queued behind it. |
.github/workflows/release.yaml:90-470 |
No |
| P3-4 | P3 | Tests | The …/kollect@* case arm sets found_controller_image=1 then always fails, since EXPECTED_IMAGE is only built in tag form — a digest-pinned entry is rejected with a misleading "appVersion" message. Also, a future kollect-pipeline entry falls through unchecked. |
hack/test/dist_artifacthub_chart_test.sh:63-70 |
No |
Adversarial checks that did not produce a finding
- Could the moved steps flip
needs.release.resultand skipoperatorhub-pr? No — all three arecontinue-on-error, the reporter included, and the reporter is the job's last step. - Does
if: steps.setup-oras.outcome == 'success'dropping the implicitsuccess()guard let the push run after a failed publish? No —setup-orashas noif:, so it is skipped on any earlier failure and the condition is false. - Did D6 delete a marker while implementing only part of it? No —
artifacthub-repo.yml:1-9carries a dated2026-08-07 — PENDING OPERATOR ACTIONmarker naming the exact remediation, and ADR-0708:83-84 still records the placeholder-until-registration consequence. The old assertion was an inverted gate that would have gone red the day someone did the right thing. - Was anything else silently dropped? No. D7–D12 untouched as instructed; the only additional deferral (namespaced
permissions:) is disclosed and filed above as P2-2.
Functional correctness
All six acceptance criteria are implemented and regression-gated by a test that I independently proved fails when the fix is reverted. The release job's own contract is preserved: hub failure can no longer strand a signed tag without a GitHub Release, and it can no longer suppress operatorhub-pr.
Coverage delta: n/a — no Go production code changed; all new logic is shell meta-tests, each mutation-verified above.
Summary / next steps: Clear to merge from my side. The two P2s are follow-ups (a docs/RELEASE.md paragraph on the second approval prompt + PAT placement, and extending the RBAC drift gate to the namespaced permissions: block); the P3s are polish. Note I do not authorise merges and have deliberately not used gh pr review — the maintainer merges.
Record the Artifact Hub and OperatorHub distribution decision under theme 07 as Exploring/Proposed until maintainer LGTM and hub registration land.
Add operator, category, capability, CRD, link, and image annotations so Artifact Hub can classify Kollect once the chart repo is registered.
Publish artifacthub-repo.yml to ghcr.io/<owner>/kollect:artifacthub.io after helm push, cosign, and the DR-FIND-07 collision guard.
Hand-templated registry+v1 bundle for package kollect/stable with digest-pinned CSV deployment and mechanical CRD copy from config/crd/bases.
Add dual-upstream operatorhub-pr.sh with DRY_RUN support and a soft-failing release job gated on OPERATORHUB_PAT for community-operators submissions.
Document conditional Artifact Hub and OperatorHub install wording without live listing badge URLs until registration and upstream merge complete.
Cover theme 07 README indexing for the hub distribution ADR.
Wire dist_* scripts into ci.yaml before lint, fix DRY_RUN cwd, add docs verify hooks, and note OLM webhook-disabled install path.
The chart test only grepped the ghcr.io/platformrelay/kollect: prefix, so the hardcoded artifacthub.io/images tag passed forever regardless of version. An explicit images list overrides Artifact Hub's automatic image extraction, so a drifted tag makes the hub advertise and security-scan the previous release's image. Parse appVersion and the annotation from the same Chart.yaml with yq and fail loudly on mismatch.
Step 3 told the maintainer to edit only version/appVersion, leaving the hardcoded Artifact Hub image tag stale by construction from the next release onward.
The setup-oras and Push Artifact Hub metadata steps sat in the critical path of
the release job with no continue-on-error, ahead of Publish GitHub Release. An
oras download, login or push failure therefore aborted the job after the
controller image, pipeline image and Helm chart were already pushed AND
cosign-signed, leaving a tag with published signed GHCR artifacts but no GitHub
Release and none of its assets (install.yaml, SBOMs, checksums, provenance,
sigstore bundles).
Move both steps after Publish GitHub Release and mark them continue-on-error, as
ADR-0708 already contracts ("soft-fail hub jobs preserve tag-release success").
Soft-fail is not silent: a new reporting step reads steps.<id>.outcome (which
continue-on-error does not mask, unlike .conclusion) and emits a ::warning::
annotation plus a GITHUB_STEP_SUMMARY line. dist_artifacthub_release_test.sh now
gates the ordering, the soft-fail flags and the reporting so this cannot regress.
…oven SHA
The operatorhub-pr job carries secrets.OPERATORHUB_PAT — a cross-repo write
credential for k8s-operatorhub/community-operators and
redhat-openshift-ecosystem/community-operators-prod — but declared no
environment, so the token was reachable without the protection the release job
documents at its own environment declaration.
It also checked out ${{ needs.release.outputs.tag }}, a mutable ref, while the
release job deliberately binds to the immutable SHA proven by eligibility
("never the mutable tag ref alone"). Add needs: [eligibility, release] and check
out needs.eligibility.outputs.sha instead.
The submission step's soft-fail is now reported through steps.<id>.outcome with a
::warning:: annotation and a job-summary line rather than being silently green.
dist_operatorhub_pr_test.sh gates all four properties.
The CSV clusterPermissions block is a hand copy of the controller-gen-generated ClusterRole and nothing kept the two in sync. The next +kubebuilder:rbac change would have shipped an under-privileged OLM bundle that 403s at runtime while the Helm chart kept working. Compare the generated bundle CSV against role.yaml with an order-insensitive normalisation, with non-empty guards on both sides so a mistyped yq path cannot make the gate pass vacuously.
The gate required the literal placeholder repositoryID, so CI would have gone red the moment the operator pasted the real Artifact Hub ID after registration — the check blocked its own remediation. Assert a well-formed UUID instead, and re-document the still-pending registration as a dated marker in artifacthub-repo.yml so the context the old assertion carried is not lost.
The two new outcome-reporting steps ran with set -euo pipefail and no continue-on-error, so a failure inside the reporter would have failed the release job after the signed publish -- exactly the failure mode the reordering exists to prevent -- and would also have flipped needs.release.result away from 'success', silently skipping operatorhub-pr. Mark both soft-fail and extend the meta-tests so no post-publish step can regress to hard-fail.
ADR-0708's contract described the OperatorHub job only as PAT-gated and soft-fail. Record the protected-environment gate, the eligibility-proven SHA checkout, the RBAC drift gate, and the rule that hub steps run after the release publish and report failures visibly.
264a01d to
2b5ec9e
Compare
|



Summary
Lands the ADR-0708 hub-distribution lane:
hack/test/dist_*_test.sh) wired into CI lintReview round 2 — REQUEST_CHANGES fixes
setup-oras+Push Artifact Hub metadatamoved afterPublish GitHub Releaseand markedcontinue-on-error: true, matching ADR-0708's "soft-fail hub jobs preserve tag-release success". Soft-fail is no longer silent: new reporting steps readsteps.<id>.outcome(not.conclusion, whichcontinue-on-errorpins tosuccess) and emit a::warning::annotation plus a$GITHUB_STEP_SUMMARYline. Same treatment applied to the already-soft-fail OperatorHub submission step; the reporting steps are themselvescontinue-on-errorso nothing after the signed publish can abort the job.dist_artifacthub_chart_test.shnow parsesappVersionand theartifacthub.io/imagesentry from the sameChart.yamlwithyqand fails unless the tag equalsv<appVersion>.docs/RELEASE.mdstep 3 now tells the maintainer to bump the annotation alongsideversion/appVersion.operatorhub-prjob now declaresenvironment: release.operatorhub-prnowneeds: [eligibility, release]and checks out${{ needs.eligibility.outputs.sha }}instead of the mutable tag ref.dist_olm_bundle_test.shgained an RBAC drift gate comparing the generated bundle CSVclusterPermissionsagainstconfig/rbac/role.yaml(order-insensitive, with non-empty guards on both sides so a mistyped path cannot pass vacuously).dist_artifacthub_release_test.shno longer requires the placeholderrepositoryID(which blocked its own remediation); it asserts a well-formed UUID. The pending-registration fact is re-documented as a dated marker inartifacthub-repo.yml.Every new assertion was red-stepped: the version-mismatch, step-ordering, missing
continue-on-error, mutable-tag-ref, missing-environment, RBAC-verb-drift and malformed-UUID perturbations each fail the corresponding gate, and all revert clean.ADR-0708's contract was updated to match: protected-environment gate, eligibility-proven SHA checkout, RBAC drift gate, and "hub steps run last and report failures visibly".
D7–D12 from the review were left as operator notes, per the review's own scoping. Also deliberately not gated: the CSV's namespaced
permissions:block (leader election) againstconfig/rbac/leader_election_role.yaml— D5 scoped the drift gate toclusterPermissionsvsconfig/rbac/role.yaml, which is where a+kubebuilder:rbacchange actually lands.OPERATORHUB_PATmust be moved from a repository secret to areleaseenvironment secret. D3 puts theoperatorhub-prjob behindenvironment: release; a repo-level secret is not visible to an environment-scoped job, so the step would see an empty token and self-skip ("OPERATORHUB_PAT not configured; skipping OperatorHub submission"). This cannot be done from a PR.releaseenvironment now also gateoperatorhub-pr. That is the intended trade for not exposing a cross-repo write credential, but it means a second approval prompt per release. If that is unwanted, create a dedicatedoperatorhubenvironment with the PAT and no reviewers instead.artifacthub-repo.ymlcarries the placeholderrepositoryIDwith a dated marker; paste the real ID from the Artifact Hub control panel after registering the chart repository. CI accepts any well-formed UUID, so this no longer turns the build red.