Skip to content
268 changes: 219 additions & 49 deletions .github/workflows/next-container-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ name: Build and push operator, bundle, and catalog images

on:
push:
branches:
branches:
- main
- rhdh-1.[0-9]+
- 1.[0-9]+.x
- release-1.[0-9]+
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand All @@ -17,19 +18,17 @@ env:
REGISTRY: ${{ vars.REGISTRY }}

jobs:
next-build:
name: Next build
changes:
name: Check for changes
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
any_changed: ${{ steps.changed-files.outputs.any_changed }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0

# check changes in this commit for regex include and exclude matches
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6
Expand All @@ -51,73 +50,244 @@ jobs:
**/*.md
**/*.adoc
.rhdh/**
tests/**
tests/**

- name: List all changed files (for troubleshooting)
env:
ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
run: |
for file in ${ALL_CHANGED_FILES}; do
echo "$file was changed"
done
build:
name: Build (${{ matrix.os }})
needs: changes
if: needs.changes.outputs.any_changed == 'true'
strategy:
fail-fast: false
matrix:
os:
- ubuntu-24.04
- ubuntu-24.04-arm
runs-on: ${{ matrix.os }}
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0

- name: Get the last commit short SHA
# run this stage only if there are changes that match the includes and not the excludes
if: steps.changed-files.outputs.any_changed == 'true'
# Skip remaining steps if no relevant changes detected
- name: Prepare
if: needs.changes.outputs.any_changed == 'true'
env:
MATRIX_OS: ${{ matrix.os }}
run: |
set -euo pipefail
SHORT_SHA=$(git rev-parse --short HEAD)
echo "SHORT_SHA=$SHORT_SHA" >> $GITHUB_ENV
BASE_VERSION=$(grep -E "^VERSION \?=" Makefile | sed -r -e "s/.+= //") # 0.0.1
echo "BASE_VERSION=$BASE_VERSION" >> $GITHUB_ENV
echo "SHORT_SHA=${SHORT_SHA}" >> "$GITHUB_ENV"
BASE_VERSION=$(grep -E "^VERSION \?=" Makefile | sed -r -e "s/.+= //")
echo "BASE_VERSION=${BASE_VERSION}" >> "$GITHUB_ENV"

case "${MATRIX_OS}" in
ubuntu-24.04)
platform="linux/amd64"
;;
ubuntu-24.04-arm)
platform="linux/arm64"
;;
*)
echo "ERROR: Unknown runner OS: ${MATRIX_OS}"
exit 1
;;
esac
echo "PLATFORM=${platform}" >> "$GITHUB_ENV"
echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV"
echo "PLATFORM_ARCH=${platform#*/}" >> "$GITHUB_ENV"

BRANCH_REF="${{ github.ref_name }}"
if [[ "${BRANCH_REF}" == "main" ]]; then
LATEST_NEXT="next"
else
LATEST_NEXT="latest"
fi
echo "LATEST_NEXT=${LATEST_NEXT}" >> "$GITHUB_ENV"

- name: Setup Go
# run this stage only if there are changes that match the includes and not the excludes
if: steps.changed-files.outputs.any_changed == 'true'
if: needs.changes.outputs.any_changed == 'true'
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6
with:
go-version-file: 'go.mod'

- name: Login to registry (${{env.REGISTRY}})
# run this stage only if there are changes that match the includes and not the excludes
if: steps.changed-files.outputs.any_changed == 'true'
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4
- name: Login to registry (${{ env.REGISTRY }})
if: needs.changes.outputs.any_changed == 'true'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ vars.QUAY_USERNAME }}
password: ${{ secrets.QUAY_TOKEN }}

- name: Build and push operator, bundle, and catalog images
# run this stage only if there are changes that match the includes and not the excludes
if: steps.changed-files.outputs.any_changed == 'true'
- name: Build and push per-arch operator, bundle, and catalog images
if: needs.changes.outputs.any_changed == 'true'
run: |
# install skopeo, podman
set -euo pipefail
sudo apt-get -y update; sudo apt-get -y install skopeo podman

export CONTAINER_TOOL=podman
latestNext="next"
# for main branch, use next tags; for 1.y branches, use :latest tags
if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then
latestNext="latest"
fi
export VERSION="${{ env.BASE_VERSION }}"
export REGISTRY_WITH_ORG="${{ env.REGISTRY }}/${{ vars.REGISTRY_ORG }}"
export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME || 'operator' }}"
export IMAGE_TAG_BASE="${REGISTRY_WITH_ORG}/${OPERATOR_IMAGE_NAME}"

: "${PLATFORM:?PLATFORM must be set}"
: "${SHORT_SHA:?SHORT_SHA must be set}"
: "${LATEST_NEXT:?LATEST_NEXT must be set}"

export VERSION=${{ env.BASE_VERSION }}
export REGISTRY_WITH_ORG=${{ env.REGISTRY }}/${{ env.REGISTRY_ORG }}
export OPERATOR_IMAGE_NAME=${OPERATOR_IMAGE_NAME:-operator}
export IMAGE_TAG_BASE=${REGISTRY_WITH_ORG}/${OPERATOR_IMAGE_NAME}
# Build all 3 images for this architecture
CONTAINER_TOOL="${CONTAINER_TOOL}" VERSION="${VERSION}" PLATFORM="${PLATFORM}" make release-build

set -ex
# Push per-arch tagged images
for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-bundle" "${OPERATOR_IMAGE_NAME}-catalog"; do
podman tag "${REGISTRY_WITH_ORG}/${image}:${VERSION}" "${REGISTRY_WITH_ORG}/${image}:${VERSION}-${{ env.PLATFORM_ARCH }}"
podman push -q "${REGISTRY_WITH_ORG}/${image}:${VERSION}-${{ env.PLATFORM_ARCH }}"

# build 4 container images with a 14d expiry
CONTAINER_TOOL=${CONTAINER_TOOL} VERSION=${VERSION} make release-build
podman tag "${REGISTRY_WITH_ORG}/${image}:${VERSION}" "${REGISTRY_WITH_ORG}/${image}:${VERSION}-${SHORT_SHA}-${{ env.PLATFORM_ARCH }}"
podman push -q "${REGISTRY_WITH_ORG}/${image}:${VERSION}-${SHORT_SHA}-${{ env.PLATFORM_ARCH }}"

# now copy images from local cache to quay, using 0.0.1-next-f00cafe, 0.0.1-next, and next tags
for image in ${OPERATOR_IMAGE_NAME} ${OPERATOR_IMAGE_NAME}-bundle ${OPERATOR_IMAGE_NAME}-catalog; do
podman push -q ${REGISTRY_WITH_ORG}/${image}:${VERSION} docker://${REGISTRY_WITH_ORG}/${image}:${VERSION}
skopeo --insecure-policy copy --all docker://${REGISTRY_WITH_ORG}/${image}:${VERSION} docker://${REGISTRY_WITH_ORG}/${image}:${VERSION}-${{ env.SHORT_SHA }}
skopeo --insecure-policy copy --all docker://${REGISTRY_WITH_ORG}/${image}:${VERSION} docker://${REGISTRY_WITH_ORG}/${image}:${latestNext}
podman tag "${REGISTRY_WITH_ORG}/${image}:${VERSION}" "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-${{ env.PLATFORM_ARCH }}"
podman push -q "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-${{ env.PLATFORM_ARCH }}"
done
env:
REGISTRY_ORG: ${{ vars.REGISTRY_ORG }}
OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME }}
# to avoid throttling on RHD org, use GH token
GH_TOKEN: ${{ secrets.RHDH_BOT_TOKEN }}

- name: Upload build marker
if: needs.changes.outputs.any_changed == 'true'
run: |
set -euo pipefail
mkdir -p /tmp/build-markers
echo "${{ env.PLATFORM_ARCH }}" > /tmp/build-markers/${{ env.PLATFORM_ARCH }}

- name: Upload build marker artifact
if: needs.changes.outputs.any_changed == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build-marker-${{ env.PLATFORM_PAIR }}
path: /tmp/build-markers/*
if-no-files-found: error
retention-days: 1

merge:
name: Create multi-arch manifests
runs-on: ubuntu-latest
needs:
- changes
- build
if: always() && needs.changes.outputs.any_changed == 'true' && needs.build.result == 'success'
permissions:
contents: read
packages: write
env:
HAS_QUAY_OAUTH: ${{ secrets.QUAY_OAUTH_TOKEN != '' }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0

- name: Download build markers
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: /tmp/build-markers
pattern: build-marker-*
merge-multiple: true

Comment on lines +176 to +200

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

3. Merge runs without builds 🐞 Bug ☼ Reliability

All build/push and marker upload steps are gated by any_changed == 'true', but the merge job
runs unconditionally and will attempt to download markers and create manifests even when nothing was
built. This can fail the workflow (no artifacts/tags) on pushes that only change ignored files
(e.g., docs/tests).
Agent Prompt
## Issue description
`merge` always runs after the matrix job, but the matrix build/push steps are skipped when `changed-files` reports `any_changed != 'true'`. In that case, `merge` still tries to download build markers and create manifests from tags that were never pushed.

## Issue Context
The build job already computes `steps.changed-files.outputs.any_changed`, but that value is not available to `merge` and the build marker artifacts are only uploaded when changes exist.

## Fix Focus Areas
- .github/workflows/next-container-build.yaml[39-62]
- .github/workflows/next-container-build.yaml[146-160]
- .github/workflows/next-container-build.yaml[161-183]

## Implementation notes
Recommended approach:
1. Add a new `changes` job that runs `tj-actions/changed-files` once and exposes `outputs.any_changed`.
2. Add job-level `if: needs.changes.outputs.any_changed == 'true'` to both `build` and `merge`.
3. Update `merge.needs` to include `changes`.

Alternative (less clean): re-run `changed-files` in `merge` and gate the manifest/cleanup steps on its result.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

- name: Prepare
run: |
set -euo pipefail
SHORT_SHA=$(git rev-parse --short HEAD)
echo "SHORT_SHA=${SHORT_SHA}" >> "$GITHUB_ENV"
BASE_VERSION=$(grep -E "^VERSION \?=" Makefile | sed -r -e "s/.+= //")
echo "BASE_VERSION=${BASE_VERSION}" >> "$GITHUB_ENV"

BRANCH_REF="${{ github.ref_name }}"
if [[ "${BRANCH_REF}" == "main" ]]; then
LATEST_NEXT="next"
else
LATEST_NEXT="latest"
fi
echo "LATEST_NEXT=${LATEST_NEXT}" >> "$GITHUB_ENV"

echo "Build markers found:"
ls -la /tmp/build-markers/ || echo "No markers found"

- name: Login to registry (${{ env.REGISTRY }})
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ vars.QUAY_USERNAME }}
password: ${{ secrets.QUAY_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Create manifest lists and push
run: |
set -euo pipefail
export REGISTRY_WITH_ORG="${{ env.REGISTRY }}/${{ vars.REGISTRY_ORG }}"
export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME || 'operator' }}"

: "${BASE_VERSION:?BASE_VERSION must be set}"
: "${SHORT_SHA:?SHORT_SHA must be set}"
: "${LATEST_NEXT:?LATEST_NEXT must be set}"

for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-bundle" "${OPERATOR_IMAGE_NAME}-catalog"; do
echo "=== Creating manifest list for ${image} ==="

# Create manifest list for version tag
docker buildx imagetools create \
-t "${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}" \
"${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-amd64" \
"${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-arm64"

# Create manifest list for version-sha tag (using SHA-scoped per-arch tags)
docker buildx imagetools create \
-t "${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-${SHORT_SHA}" \
"${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-${SHORT_SHA}-amd64" \
"${REGISTRY_WITH_ORG}/${image}:${BASE_VERSION}-${SHORT_SHA}-arm64"

# Create manifest list for latestNext tag (next or latest)
docker buildx imagetools create \
-t "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}" \
"${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-amd64" \
"${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}-arm64"

echo "=== Inspecting manifest for ${image}:${LATEST_NEXT} ==="
docker buildx imagetools inspect "${REGISTRY_WITH_ORG}/${image}:${LATEST_NEXT}"
done
Comment on lines +202 to +263

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Missing set -u strictness 📘 Rule violation ☼ Reliability

New GitHub Actions run scripts use set -ex (or no strict mode) but omit set -u and do not
validate required environment variables, which can mask unset-variable failures. The scripts also
use unquoted variable expansions (e.g., >> $GITHUB_ENV, ${REGISTRY_WITH_ORG}/${image}...),
increasing brittleness and ShellCheck-style compliance risk.
Agent Prompt
## Issue description
The workflow `run` scripts are not hardened: they omit `set -euo pipefail` (notably `-u`), do not validate required env vars, and contain unquoted variable expansions.

## Issue Context
PR Compliance ID 5 requires strict modes, quoting, and env validation for shell scripts embedded in workflows.

## Fix Focus Areas
- .github/workflows/next-container-build.yaml[71-99]
- .github/workflows/next-container-build.yaml[114-140]
- .github/workflows/next-container-build.yaml[184-242]
- .github/workflows/next-container-build.yaml[247-264]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

env:
REGISTRY_ORG: ${{ vars.REGISTRY_ORG }}
OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME }}

- name: Cleanup per-arch tags
if: env.HAS_QUAY_OAUTH == 'true'
run: |
set -euo pipefail
export OPERATOR_IMAGE_NAME="${{ vars.OPERATOR_IMAGE_NAME || 'operator' }}"
export NAMESPACE="${{ vars.REGISTRY_ORG }}"

: "${BASE_VERSION:?BASE_VERSION must be set}"
: "${SHORT_SHA:?SHORT_SHA must be set}"
: "${LATEST_NEXT:?LATEST_NEXT must be set}"

for image in "${OPERATOR_IMAGE_NAME}" "${OPERATOR_IMAGE_NAME}-bundle" "${OPERATOR_IMAGE_NAME}-catalog"; do
for arch in amd64 arm64; do
for tag in "${BASE_VERSION}-${arch}" "${BASE_VERSION}-${SHORT_SHA}-${arch}" "${LATEST_NEXT}-${arch}"; do
echo "Deleting per-arch tag: ${image}:${tag}"
curl -s -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: Bearer ${QUAY_OAUTH_TOKEN}" \
"https://quay.io/api/v1/repository/${NAMESPACE}/${image}/tag/${tag}" \
|| true
done
done
done
env:
REGISTRY_ORG: ${{ vars.REGISTRY_ORG }}
OPERATOR_IMAGE_NAME: ${{ vars.OPERATOR_IMAGE_NAME }}
QUAY_OAUTH_TOKEN: ${{ secrets.QUAY_OAUTH_TOKEN }}