diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72bec0abc..476e039f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -591,6 +591,7 @@ jobs: outputs: primary-tag: ${{ steps.meta.outputs.primary-tag }} is-release: ${{ steps.meta.outputs.is-release }} + is-stable: ${{ steps.meta.outputs.is-stable }} permissions: contents: read # checkout + build @@ -1115,32 +1116,17 @@ jobs: - name: Preflight check (pushed image) env: - REDHAT_API_TOKEN: ${{ secrets.REDHAT_API_TOKEN }} - REDHAT_CERT_PROJECT_ID: ${{ secrets.REDHAT_CERT_PROJECT_ID }} + PRIMARY_TAG: ${{ needs.docker.outputs.primary-tag }} run: | - PRIMARY_TAG="${{ needs.docker.outputs.primary-tag }}" - IS_RELEASE="${{ needs.docker.outputs.is-release }}" - - # On release tags, submit results to Red Hat Partner Connect - SUBMIT_FLAG="" - if [ "$IS_RELEASE" = "true" ]; then - if [ -n "$REDHAT_API_TOKEN" ] && [ -n "$REDHAT_CERT_PROJECT_ID" ]; then - # Pass credentials via preflight's native env vars (avoids process-listing exposure) - export PFLT_PYXIS_API_TOKEN="$REDHAT_API_TOKEN" - export PFLT_CERTIFICATION_COMPONENT_ID="$REDHAT_CERT_PROJECT_ID" - SUBMIT_FLAG="--submit" - echo "📤 Release tag detected — submitting to Red Hat Partner Connect" - else - echo "::warning::Release tag detected but REDHAT_API_TOKEN or REDHAT_CERT_PROJECT_ID secret is missing — running dry-run only" - fi - else - echo "🔍 Non-release push — running preflight in dry-run mode (no submit)" - fi - + # Verification only - no submission. The certification project is a + # HOSTED-registry project, and Pyxis rejects submissions for a + # docker.io-hosted image ("The 'container.registry' field is + # immutable for projects with hosted registry"), which reddened + # every release tag. Submission happens exclusively in + # redhat-certify.yml, against the hosted-registry copy. preflight check container \ docker.io/${DOCKER_IMAGE}:${PRIMARY_TAG} \ --docker-config=${HOME}/.docker/config.json \ - ${SUBMIT_FLAG} \ 2>&1 | tee preflight-push-results.txt PREFLIGHT_EXIT=${PIPESTATUS[0]} echo "" @@ -1151,9 +1137,6 @@ jobs: exit 1 else echo "✅ All preflight checks passed on pushed image" - if [ -n "$SUBMIT_FLAG" ]; then - echo "📤 Results submitted to Red Hat Partner Connect" - fi fi - name: Upload preflight results @@ -1166,6 +1149,22 @@ jobs: artifacts/ retention-days: 30 + # ─── Red Hat Catalog Publish ─────────────────────────────────── + # Stable release tags only (X.Y.Z, no pre-release suffix): publishes the + # just-released image to Red Hat's hosted certification registry and submits + # the preflight results. Reuses redhat-certify.yml, which is also manually + # dispatchable for re-submissions with a bumped release number. + redhat-publish: + name: Red Hat Catalog Publish + needs: [docker, smoke-test] + if: startsWith(github.ref, 'refs/tags/') && needs.docker.outputs.is-stable == 'true' + uses: ./.github/workflows/redhat-certify.yml + with: + version: ${{ needs.docker.outputs.primary-tag }} + release: "1" + submit: "true" + secrets: inherit + # ─── Job 7: Slack Notification ───────────────────────────────── notify-slack: name: Slack Notification diff --git a/.github/workflows/redhat-certify.yml b/.github/workflows/redhat-certify.yml index d72f5dd74..7c135ab5f 100644 --- a/.github/workflows/redhat-certify.yml +++ b/.github/workflows/redhat-certify.yml @@ -1,6 +1,41 @@ name: Red Hat Certification Release +# Publishes an already-released image to Red Hat's HOSTED certification +# registry and submits the preflight results. EDDI distributes on two +# registries: docker.io (published by ci.yml on the release tag) and Red Hat's +# catalog (published here, per release, after the fact). +# +# The certification project is a "hosted registry" project: Pyxis requires the +# certified image to live in quay.io/redhat-isv-containers/, from +# where Red Hat serves it to customers via registry.connect.redhat.com. +# Submitting results for a docker.io-hosted image is rejected with +# "The 'container.registry' field is immutable for projects with hosted +# registry" - which is how this flow's predecessor failed. +# +# Required secrets, all from the certification project in Partner Connect: +# REDHAT_CERT_PROJECT_ID - the project id (also the hosted repo name) +# REDHAT_REGISTRY_USERNAME - the project's registry robot user +# (shown with the key on the "Registry key" page) +# REDHAT_REGISTRY_KEY - the project's registry key (the robot password) +# REDHAT_API_TOKEN - Pyxis API token, for --submit + on: + # Called automatically by ci.yml on every stable release tag, and manually + # dispatchable for re-submissions (bump `release` to 2, 3, ... - the version + # itself never changes for a re-submission). + workflow_call: + inputs: + version: + required: true + type: string + release: + required: false + type: string + default: "1" + submit: + required: false + type: string + default: "true" workflow_dispatch: inputs: version: @@ -17,38 +52,32 @@ on: default: "true" type: choice options: ["true", "false"] - registry: - description: "Target registry" - required: true - default: "docker.io" - type: choice - options: ["docker.io", "quay.io"] permissions: contents: read jobs: certify: - name: Certify Published Image + name: Publish to Red Hat Registry & Certify runs-on: ubuntu-latest steps: - # Certification runs against the image that was ALREADY published and signed - # by ci.yml on the release tag. It deliberately does NOT rebuild: a rebuild - # produces a different digest, is not covered by the release's cosign - # signature or SLSA attestation, and pushing it over : and :latest - # would replace what users pull with unsigned bytes. + # Certification runs against the image that was ALREADY published and + # signed by ci.yml on the release tag. It deliberately does NOT rebuild: + # a rebuild produces a different digest, is not covered by the release's + # cosign signature or SLSA attestation, and would put bytes in Red Hat's + # catalog that differ from what docker.io users pull. # - # Inputs reach the shell through `env:`, never through ${{ }} interpolation. - # Interpolation splices the value into the script text before Bash parses - # it, so a crafted version/release could close the quoting and run commands - # with the registry credentials this job holds. + # Inputs reach the shell through `env:`, never through ${{ }} + # interpolation. Interpolation splices the value into the script text + # before Bash parses it, so a crafted version/release could close the + # quoting and run commands with the registry credentials this job holds. - name: Set image coordinates id: image env: - REGISTRY: ${{ inputs.registry }} VERSION: ${{ inputs.version }} RELEASE: ${{ inputs.release }} + PROJECT_ID: ${{ secrets.REDHAT_CERT_PROJECT_ID }} run: | set -euo pipefail case "${VERSION}" in @@ -59,20 +88,16 @@ jobs: ''|*[!0-9]*|0|0*) echo "::error::release must be a positive integer without leading zeros (got: ${RELEASE})"; exit 1;; esac + if [ -z "${PROJECT_ID}" ]; then + echo "::error::REDHAT_CERT_PROJECT_ID secret is not set"; exit 1 + fi - IMAGE="${REGISTRY}/labsai/eddi" { - echo "released=${IMAGE}:${VERSION}" - echo "certified=${IMAGE}:${VERSION}-${RELEASE}" + echo "released=docker.io/labsai/eddi:${VERSION}" + echo "hosted=quay.io/redhat-isv-containers/${PROJECT_ID}:${VERSION}" + echo "certified=quay.io/redhat-isv-containers/${PROJECT_ID}:${VERSION}-${RELEASE}" } >> "$GITHUB_OUTPUT" - - name: Login to registry - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 - with: - registry: ${{ inputs.registry }} - username: ${{ inputs.registry == 'quay.io' && secrets.QUAY_USERNAME || secrets.DOCKER_USERNAME }} - password: ${{ inputs.registry == 'quay.io' && secrets.QUAY_PASSWORD || secrets.DOCKER_PASSWORD }} - - name: Pull the published release image id: pull env: @@ -107,18 +132,29 @@ jobs: echo "=== THIRD-PARTY.txt in container ===" docker run --rm --entrypoint="" "${RELEASED}" head -30 /licenses/THIRD-PARTY.txt || true - # Red Hat's catalogue keys on a version-release coordinate. Retagging the - # pulled image reuses its manifest, so this tag carries the SAME digest as - # the released one - a rename, not a rebuild. : and :latest are - # never pushed here. - - name: Publish the version-release coordinate + - name: Login to the Red Hat hosted registry + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4 + with: + registry: quay.io + username: ${{ secrets.REDHAT_REGISTRY_USERNAME }} + password: ${{ secrets.REDHAT_REGISTRY_KEY }} + + # Retagging the pulled image reuses its manifest, so the hosted tags + # carry the SAME digest as the docker.io release - a rename, not a + # rebuild. Two tags land in the hosted repo: is what the + # catalog offers customers, - is the coordinate this + # certification attempt runs against. docker.io is never pushed here. + - name: Publish to the hosted repository env: RELEASED: ${{ steps.image.outputs.released }} + HOSTED: ${{ steps.image.outputs.hosted }} CERTIFIED: ${{ steps.image.outputs.certified }} EXPECTED: ${{ steps.pull.outputs.digest }} run: | set -euo pipefail + docker tag "${RELEASED}" "${HOSTED}" docker tag "${RELEASED}" "${CERTIFIED}" + docker push "${HOSTED}" docker push "${CERTIFIED}" PUSHED="$(docker buildx imagetools inspect "${CERTIFIED}" --format '{{.Manifest.Digest}}')" diff --git a/docs/changelog.md b/docs/changelog.md index 22c116ec6..7ac37052a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -601,6 +601,55 @@ that instead of naming a type. A store can no longer query a namespace it does n `DescriptorTypeConsistencyTest` sweeps the sources and fails on any hard-coded type that disagrees with its store's `resourceURI` — verified by reverting the fix. Two existing tests had pinned the wrong value (`ai.labs.httpcalls`) and were corrected. +## 📮 feat(ci): publish releases to Red Hat's hosted registry, distributing on both registries (2026-08-20) + +**Repo:** EDDI (`feat/redhat-hosted-registry`) + +The first certification run after the preflight fixes cleared every check but died at submission: + +```text +could not submit to pyxis: 400: "The 'container.registry' field is immutable +for projects with hosted registry" +``` + +The certification project is a **hosted registry** project — Pyxis requires the certified image to +live in `quay.io/redhat-isv-containers/`, from where Red Hat serves customers via +`registry.connect.redhat.com`. Nothing in this repo had ever pushed there: `ci.yml` publishes only +`docker.io/labsai/eddi`, and the certify workflow's old `quay.io` option targeted the generic +`quay.io/labsai/eddi`, which satisfies neither model. The decision is to distribute on **both** +registries: Docker Hub stays the primary (published by `ci.yml` on the release tag, unchanged), and +the certify workflow becomes the Red Hat publication path. + +`redhat-certify.yml` now pulls the released `docker.io/labsai/eddi:`, retags it into the +hosted repository as `` (the customer-facing tag) and `-` (this attempt's +coordinate), pushes only those two, asserts at the registry that the pushed digest equals the +released digest, and runs preflight with `--submit` against the hosted coordinate. All the +invariants from the previous rewrite carry over: no rebuild ever, inputs reach the shell only via +`env:`, version/release format-validated, digests read via `buildx imagetools` (registry-side, not +local cache), preflight pinned at 1.20.0 by version and SHA256. The `registry` dispatch input is +gone — the source is always docker.io and the destination is always the hosted repo, so a choice +there could only misdirect. + +**Two new secrets are required before the first run**, both from the certification project's +"Registry key" page in Partner Connect: `REDHAT_REGISTRY_USERNAME` (the robot user) and +`REDHAT_REGISTRY_KEY` (its password). `docs/redhat-openshift.md` rewritten to describe the +two-registry model, the no-rebuild rationale, and the full secret set. + +6.3.0 still needs no re-release: once the secrets exist, `version=6.3.0`, `release=1` certifies the +shipped digest `sha256:202c0412…` — the failed submission attempt consumed nothing. + +**Automated per release, and a guaranteed-red trap removed.** Follow-up on the same branch: + +- `redhat-certify.yml` gained a `workflow_call` trigger, and `ci.yml` gained a `redhat-publish` job + that calls it after the smoke test on every **stable** release tag (`X.Y.Z` only — the `is-stable` + output the docker job already computed is now exposed and gates it, so RCs never reach the + catalog). `version` comes from the tag, `release` is `1`; re-submissions stay manual via + `workflow_dispatch` with a bumped release number. Secrets flow via `secrets: inherit`. +- `ci.yml`'s **Preflight Verify (Pushed Image) no longer submits to Pyxis.** It submitted on every + release tag against the docker.io image — which, against a hosted-registry project, is exactly the + 400 the certify run hit. Left alone, every future release tag would have gone red at that step + even with the certify workflow fixed. It is now verification-only; submission lives solely in + `redhat-certify.yml` against the hosted copy. --- diff --git a/docs/redhat-openshift.md b/docs/redhat-openshift.md index 2cfe159a4..5d0542ab2 100644 --- a/docs/redhat-openshift.md +++ b/docs/redhat-openshift.md @@ -51,20 +51,21 @@ The EDDI container image is certified by Red Hat / IBM for use on OpenShift. Cer ### Automated Certification Workflow -The certification release process is fully automated: +EDDI is distributed on **two registries**: Docker Hub (`labsai/eddi`, published by `ci.yml` when a release tag is pushed) and Red Hat's catalog (published by this workflow, per release, after the fact). The certification project uses Red Hat's **hosted registry**: the certified image lives in `quay.io/redhat-isv-containers/` and Red Hat serves it to customers via `registry.connect.redhat.com`. -1. **Build** — `mvnw clean package -Plicense-gen` builds the application and auto-generates license files via the [MojoHaus license-maven-plugin](https://www.mojohaus.org/license-maven-plugin/) -2. **Docker build** — Builds the image with Red Hat certification labels (parameterized via `--build-arg`) -3. **Push** — Pushes to Docker Hub (or Quay.io when configured) -4. **Preflight** — Runs the [Red Hat preflight tool](https://github.com/redhat-openshift-ecosystem/openshift-preflight) to validate certification requirements +The workflow certifies the image that was **already released** — it never rebuilds. A rebuild would have a different digest, would not be covered by the release's cosign signature or SLSA attestation, and would put bytes in Red Hat's catalog that differ from what Docker Hub users pull. Instead it: + +1. **Pull** — Pulls the released `docker.io/labsai/eddi:` and records its registry digest +2. **Verify** — Checks the Red Hat labels and the `/licenses` directory inside the pulled image +3. **Publish** — Retags to `quay.io/redhat-isv-containers/` as `` and `-` (a retag reuses the manifest, so the hosted tags carry the *same digest* as the release — asserted after pushing) +4. **Preflight** — Runs the [Red Hat preflight tool](https://github.com/redhat-openshift-ecosystem/openshift-preflight) against the hosted `-` coordinate 5. **Submit** — Optionally submits results to Red Hat Partner Connect for review To trigger a certification release, go to **Actions → Red Hat Certification Release → Run workflow** and provide: -- `version` — EDDI version (e.g., `6.3.0`) -- `release` — Incremental release number (e.g., `1`, `2`, `3`) +- `version` — EDDI version (e.g., `6.3.0`) — must already be released on Docker Hub +- `release` — Incremental release number (e.g., `1`, `2`, `3`) — lets the same version be re-submitted - `submit` — Whether to submit results to Red Hat (`true`/`false`) -- `registry` — Target registry (`docker.io` or `quay.io`) ### Preflight Quality Gate @@ -72,12 +73,14 @@ Every push to `main` or release tag that produces a Docker image is validated by ### Required GitHub Secrets -| Secret | Purpose | -| ------------------------ | -------------------------------------------------------- | -| `REDHAT_API_TOKEN` | Pyxis API token from Red Hat Partner Connect | -| `REDHAT_CERT_PROJECT_ID` | Certification project ID | -| `DOCKER_USERNAME` | Docker Hub username | -| `DOCKER_PASSWORD` | Docker Hub password | +| Secret | Purpose | +| -------------------------- | ------------------------------------------------------------------------------------------- | +| `REDHAT_API_TOKEN` | Pyxis API token from Red Hat Partner Connect | +| `REDHAT_CERT_PROJECT_ID` | Certification project ID (also names the hosted repository) | +| `REDHAT_REGISTRY_USERNAME` | The project's registry robot user — shown with the key on the project's **Registry key** page | +| `REDHAT_REGISTRY_KEY` | The project's registry key (the robot account's password) | +| `DOCKER_USERNAME` | Docker Hub username (used by `ci.yml`, not by certification) | +| `DOCKER_PASSWORD` | Docker Hub password (used by `ci.yml`, not by certification) | | `QUAY_USERNAME` | Quay.io robot account (optional, for Quay.io publishing) | | `QUAY_PASSWORD` | Quay.io password (optional) |