name: Red Hat Certification # Certify published UBI images in the Red Hat Ecosystem Catalog. Called by # release.yml on stable tags, or run by hand to (re)certify any released # version. preflight submits every architecture of an image's manifest list # to Pyxis; auto-publish on the component makes it public once certified. # # Each component's Partner Connect ID comes from the REDHAT_CERT_ID_ # repository variable, e.g. REDHAT_CERT_ID_CLIENT_ROOTLESS. The run fails # before certifying anything if a selected component's variable is not set. on: workflow_call: inputs: component: type: string required: true version: type: string required: true secrets: PYXIS_API_TOKEN: required: true workflow_dispatch: inputs: component: description: "Component to certify" type: choice required: true default: all options: - all - client-rootless - reverse-proxy version: description: "Released version, e.g. v0.80.0" type: string required: true permissions: contents: read jobs: resolve: name: Resolve components runs-on: ubuntu-24.04 outputs: version: ${{ steps.resolve.outputs.version }} matrix: ${{ steps.resolve.outputs.matrix }} steps: - name: Resolve components and images id: resolve env: COMPONENT: ${{ inputs.component }} INPUT_VERSION: ${{ inputs.version }} REPO_VARS: ${{ toJSON(vars) }} run: | set -euo pipefail version="${INPUT_VERSION#v}" if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "::error::Only stable x.y.z versions are certified, got '${INPUT_VERSION}'" exit 1 fi # name, image repository, tag suffix (must match .goreleaser.yaml). # Keep the names in sync with the workflow_dispatch options above. components=( "client-rootless ghcr.io/netbirdio/netbird -rootless-ubi" "reverse-proxy ghcr.io/netbirdio/reverse-proxy -ubi" ) matrix="[]" missing=() for c in "${components[@]}"; do read -r name repo suffix <<< "$c" [[ "$COMPONENT" == all || "$COMPONENT" == "$name" ]] || continue var="REDHAT_CERT_ID_${name^^}"; var="${var//-/_}" id="$(jq -r --arg v "$var" '.[$v] // empty' <<< "$REPO_VARS")" if [[ -z "$id" ]]; then missing+=("$var") continue fi matrix="$(jq -c --arg n "$name" --arg t "${version}${suffix}" --arg r "${repo}:${version}${suffix}" --arg i "$id" \ '. + [{component: $n, tag: $t, ref: $r, component_id: $i}]' <<< "$matrix")" done if (( ${#missing[@]} )); then echo "::error::Set these repository variables to the Partner Connect component IDs: ${missing[*]}" exit 1 fi if [[ "$matrix" == "[]" ]]; then echo "::error::No component to certify for '${COMPONENT}'" exit 1 fi echo "Components to certify: ${matrix}" echo "version=${version}" >> "$GITHUB_OUTPUT" echo "matrix=${matrix}" >> "$GITHUB_OUTPUT" certify: name: "Certify ${{ matrix.component }} UBI image" needs: resolve runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: include: ${{ fromJSON(needs.resolve.outputs.matrix) }} env: PREFLIGHT_VERSION: "1.21.0" # sha256 of preflight-linux-amd64 from the 1.21.0 GitHub release. # Red Hat publishes no checksum file, so the value is pinned here. PREFLIGHT_SHA256: "5e653135503c72f8702bbe31d7643197d12937c68086879133dd6b9650a9a449" steps: - name: Verify the multi-arch image is on ghcr.io env: IMAGE_REF: ${{ matrix.ref }} run: | set -euo pipefail docker buildx imagetools inspect "$IMAGE_REF" --raw > manifest.json for arch in amd64 arm64; do if ! jq -e --arg a "$arch" '.manifests[] | select(.platform.architecture == $a)' manifest.json > /dev/null; then echo "::error::${IMAGE_REF} has no ${arch} manifest" exit 1 fi done echo "Manifest list for ${IMAGE_REF}:" jq -r '.manifests[] | "\(.platform.os)/\(.platform.architecture) \(.digest)"' manifest.json - name: Install preflight run: | set -euo pipefail curl -fsSL --proto '=https' --proto-redir '=https' -o preflight \ "https://github.com/redhat-openshift-ecosystem/openshift-preflight/releases/download/${PREFLIGHT_VERSION}/preflight-linux-amd64" echo "${PREFLIGHT_SHA256} preflight" | sha256sum -c - chmod +x preflight ./preflight --version - name: Run preflight checks and submit to Red Hat env: IMAGE_REF: ${{ matrix.ref }} PFLT_PYXIS_API_TOKEN: ${{ secrets.PYXIS_API_TOKEN }} PFLT_CERTIFICATION_COMPONENT_ID: ${{ matrix.component_id }} PFLT_ARTIFACTS: artifacts PFLT_LOGFILE: artifacts/preflight.log PFLT_LOGLEVEL: info PFLT_JUNIT: "true" run: | set -euo pipefail # No --platform: preflight walks the manifest list and submits every # architecture in one run, grouped under one manifest-list digest. # preflight does not create the PFLT_LOGFILE directory, and --submit # fails if the log file is missing. mkdir -p artifacts ./preflight check container "$IMAGE_REF" --submit - name: Fail if any check did not pass run: | set -euo pipefail shopt -s nullglob results=(artifacts/results.json artifacts/*/results.json) if [[ ${#results[@]} -eq 0 ]]; then echo "::error::preflight produced no results.json" exit 1 fi status=0 for f in "${results[@]}"; do arch="$(basename "$(dirname "$f")")" passed="$(jq -r '.passed' "$f")" failed="$(jq -r '[.results.failed[]?.name] | join(", ")' "$f")" echo "${arch}: passed=${passed} ${failed:+failed checks: ${failed}}" [[ "$passed" == "true" ]] || status=1 done exit $status - name: Upload preflight artifacts if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: redhat-preflight-${{ matrix.component }}-${{ needs.resolve.outputs.version }} path: artifacts/ retention-days: 30 - name: Wait for Pyxis to mark both architectures certified env: TAG: ${{ matrix.tag }} COMPONENT_ID: ${{ matrix.component_id }} PFLT_PYXIS_API_TOKEN: ${{ secrets.PYXIS_API_TOKEN }} run: | set -euo pipefail # Filter on the tag server-side so older versions are found past the first page. url="https://catalog.redhat.com/api/containers/v1/projects/certification/id/${COMPONENT_ID}/images?filter=repositories.tags.name==${TAG}&page_size=100" for attempt in $(seq 1 20); do certified="$(curl -fsS --proto '=https' --proto-redir '=https' -H "X-API-KEY: ${PFLT_PYXIS_API_TOKEN}" "$url" \ | jq -r --arg t "$TAG" '[.data[] | select(.repositories[]?.tags[]?.name == $t) | select(.certified == true) | .architecture] | unique | join(",")')" echo "attempt ${attempt}: certified architectures for ${TAG}: ${certified:-none}" if [[ "$certified" == "amd64,arm64" ]]; then echo "Both architectures certified. Auto-publish is enabled on the component, so the catalog updates on its own." exit 0 fi sleep 30 done echo "::error::Pyxis has not marked both architectures certified after 10 minutes. Check https://connect.redhat.com/component/view/${COMPONENT_ID}/images" exit 1