Skip to content

Build & Push Images #353

Build & Push Images

Build & Push Images #353

Workflow file for this run

name: Build & Push Images
on:
push:
branches: [main]
tags: ["v*"]
env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}/rhacs-manager
HELM_CHART_REGISTRY: oci://ghcr.io/${{ github.repository_owner }}/charts
jobs:
build-backend:
name: Backend Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
id: meta
with:
images: ${{ env.IMAGE_PREFIX }}-backend
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern=v{{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern=v{{major}}.{{minor}}
type=sha
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: backend
file: backend/Containerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-frontend:
name: Frontend Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
id: meta
with:
images: ${{ env.IMAGE_PREFIX }}-frontend
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern=v{{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern=v{{major}}.{{minor}}
type=sha
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: frontend
file: frontend/Containerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-auth-header-injector:
name: Auth Header Injector Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
id: meta
with:
images: ${{ env.IMAGE_PREFIX }}-auth-header-injector
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern=v{{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern=v{{major}}.{{minor}}
type=sha
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: auth-header-injector
file: auth-header-injector/Containerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-mcp-server:
name: MCP Server Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
id: meta
with:
images: ${{ env.IMAGE_PREFIX }}-mcp-server
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern=v{{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern=v{{major}}.{{minor}}
type=sha
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
with:
context: mcp-server
file: mcp-server/Containerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-helm-chart:
name: Helm Chart (OCI)
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5
- name: Login to GHCR for Helm OCI push
run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ${{ env.REGISTRY }} -u "${{ github.actor }}" --password-stdin
- name: Package Helm chart
shell: bash
run: |
set -euo pipefail
mkdir -p .artifacts/helm
base_version="$(helm show chart deploy/helm/rhacs-manager | awk '/^version:/ {print $2}')"
if [[ "${{ github.ref_type }}" == "tag" ]]; then
package_version="${base_version}"
app_version="${base_version}"
else
package_version="${base_version}-main.${GITHUB_SHA::7}"
app_version="${GITHUB_SHA::7}"
fi
helm package deploy/helm/rhacs-manager \
--destination .artifacts/helm \
--version "${package_version}" \
--app-version "${app_version}"
- name: Push Helm chart to GHCR as OCI artifact
run: |
chart_archive="$(ls .artifacts/helm/rhacs-manager-*.tgz)"
helm push "${chart_archive}" "${{ env.HELM_CHART_REGISTRY }}"
create-release:
name: Create GitHub Release
if: ${{ startsWith(github.ref, 'refs/tags/') }}
needs:
- build-backend
- build-frontend
- build-auth-header-injector
- build-mcp-server
- build-helm-chart
runs-on: ubuntu-latest
permissions:
contents: write
packages: read
env:
RELEASE_TAG: ${{ github.ref_name }}
BACKEND_IMAGE: ghcr.io/${{ github.repository_owner }}/rhacs-manager-backend:${{ github.ref_name }}
FRONTEND_IMAGE: ghcr.io/${{ github.repository_owner }}/rhacs-manager-frontend:${{ github.ref_name }}
AUTH_HEADER_INJECTOR_IMAGE: ghcr.io/${{ github.repository_owner }}/rhacs-manager-auth-header-injector:${{ github.ref_name }}
MCP_SERVER_IMAGE: ghcr.io/${{ github.repository_owner }}/rhacs-manager-mcp-server:${{ github.ref_name }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
- uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5
- name: Render Helm manifests
shell: bash
run: |
set -euo pipefail
mkdir -p .artifacts/manifests
helm template rhacs-manager deploy/helm/rhacs-manager \
-n rhacs-manager \
--set frontend.oauthProxy.cookieSecret=CHANGE_ME \
> .artifacts/manifests/rhacs-manager-hub.yaml
helm template rhacs-manager-spoke deploy/helm/rhacs-manager \
-n rhacs-manager \
--set mode=spoke \
--set spoke.oauthProxy.cookieSecret=CHANGE_ME \
> .artifacts/manifests/rhacs-manager-spoke.yaml
helm template rhacs-manager-hub-mcp deploy/helm/rhacs-manager \
-n rhacs-manager \
--set frontend.oauthProxy.cookieSecret=CHANGE_ME \
--set mcp.enabled=true \
--set mcp.secret.stringData.MCP_API_KEY=CHANGE_ME \
--show-only templates/frontend-deployment.yaml \
> .artifacts/manifests/rhacs-manager-hub-mcp.yaml
helm template rhacs-manager-spoke-mcp deploy/helm/rhacs-manager \
-n rhacs-manager \
--set mode=spoke \
--set spoke.oauthProxy.cookieSecret=CHANGE_ME \
--set spoke.mcp.enabled=true \
--show-only templates/frontend-deployment.yaml \
> .artifacts/manifests/rhacs-manager-spoke-mcp.yaml
# Strip Helm-injected labels from rendered plain YAML manifests
for f in .artifacts/manifests/*.yaml; do
yq -i 'del(
.. | select(has("helm.sh/chart")).["helm.sh/chart"],
.. | select(has("app.kubernetes.io/name")).["app.kubernetes.io/name"],
.. | select(has("app.kubernetes.io/instance")).["app.kubernetes.io/instance"],
.. | select(has("app.kubernetes.io/version")).["app.kubernetes.io/version"],
.. | select(has("app.kubernetes.io/managed-by")).["app.kubernetes.io/managed-by"]
)' "$f"
yq -i 'del(.metadata.labels | select(length == 0))' "$f"
yq -i 'del(.spec.template.metadata.labels | select(length == 0))' "$f"
done
- name: Package Helm chart
shell: bash
run: |
set -euo pipefail
mkdir -p .artifacts/helm
base_version="$(helm show chart deploy/helm/rhacs-manager | awk '/^version:/ {print $2}')"
helm package deploy/helm/rhacs-manager \
--destination .artifacts/helm \
--version "${base_version}" \
--app-version "${base_version}"
- uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate changelog for current release tag
uses: orhun/git-cliff-action@v4
with:
config: cliff.toml
args: --current --verbose
env:
OUTPUT: release-notes.md
GITHUB_REPO: ${{ github.repository }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Scan backend image with Trivy
continue-on-error: true
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
env:
TRIVY_USERNAME: ${{ github.actor }}
TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
with:
scan-type: image
image-ref: ${{ env.BACKEND_IMAGE }}
scanners: vuln
format: json
output: trivy-report-backend.json
exit-code: "0"
ignore-unfixed: false
- name: Scan frontend image with Trivy
continue-on-error: true
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
env:
TRIVY_USERNAME: ${{ github.actor }}
TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
with:
scan-type: image
image-ref: ${{ env.FRONTEND_IMAGE }}
scanners: vuln
format: json
output: trivy-report-frontend.json
exit-code: "0"
ignore-unfixed: false
- name: Scan MCP server image with Trivy
continue-on-error: true
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
env:
TRIVY_USERNAME: ${{ github.actor }}
TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
with:
scan-type: image
image-ref: ${{ env.MCP_SERVER_IMAGE }}
scanners: vuln
format: json
output: trivy-report-mcp-server.json
exit-code: "0"
ignore-unfixed: false
- name: Scan auth-header-injector image with Trivy
continue-on-error: true
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
env:
TRIVY_USERNAME: ${{ github.actor }}
TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
with:
scan-type: image
image-ref: ${{ env.AUTH_HEADER_INJECTOR_IMAGE }}
scanners: vuln
format: json
output: trivy-report-auth-header-injector.json
exit-code: "0"
ignore-unfixed: false
- name: Build vulnerability summary for release notes
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
reports=(trivy-report-*.json)
{
echo
echo "## Vulnerability Scan Summary (Trivy)"
echo
echo "| Image | Total | CRITICAL | HIGH | MEDIUM | LOW | UNKNOWN |"
echo "| --- | ---: | ---: | ---: | ---: | ---: | ---: |"
} >> release-notes.md
if (( ${#reports[@]} == 0 )); then
echo "| n/a | 0 | 0 | 0 | 0 | 0 | 0 |" >> release-notes.md
echo >> release-notes.md
echo "Trivy did not produce any JSON report files in this job." >> release-notes.md
fi
for report in "${reports[@]}"; do
image_name="${report#trivy-report-}"
image_name="${image_name%.json}"
if ! jq -e . "${report}" >/dev/null 2>&1; then
echo "| ${image_name} | 0 | 0 | 0 | 0 | 0 | 0 |" >> release-notes.md
{
echo
echo "> Warning: Trivy report \`${report}\` is not valid JSON."
} >> release-notes.md
continue
fi
total="$(jq -r '[.Results[]? | (.Vulnerabilities[]?, .Packages[]?.Vulnerabilities[]?)] | length' "${report}")"
critical="$(jq -r '[.Results[]? | (.Vulnerabilities[]?, .Packages[]?.Vulnerabilities[]?) | select(.Severity == "CRITICAL")] | length' "${report}")"
high="$(jq -r '[.Results[]? | (.Vulnerabilities[]?, .Packages[]?.Vulnerabilities[]?) | select(.Severity == "HIGH")] | length' "${report}")"
medium="$(jq -r '[.Results[]? | (.Vulnerabilities[]?, .Packages[]?.Vulnerabilities[]?) | select(.Severity == "MEDIUM")] | length' "${report}")"
low="$(jq -r '[.Results[]? | (.Vulnerabilities[]?, .Packages[]?.Vulnerabilities[]?) | select(.Severity == "LOW")] | length' "${report}")"
unknown="$(jq -r '[.Results[]? | (.Vulnerabilities[]?, .Packages[]?.Vulnerabilities[]?) | select(.Severity == "UNKNOWN")] | length' "${report}")"
echo "| ${image_name} | ${total} | ${critical} | ${high} | ${medium} | ${low} | ${unknown} |" >> release-notes.md
done
{
echo
echo "## High/Critical Findings"
echo
} >> release-notes.md
if (( ${#reports[@]} == 0 )); then
{
echo "No Trivy JSON reports were generated, so no HIGH/CRITICAL details are available."
echo
} >> release-notes.md
fi
for report in "${reports[@]}"; do
image_name="${report#trivy-report-}"
image_name="${image_name%.json}"
{
echo "### ${image_name}"
echo
} >> release-notes.md
if ! jq -e . "${report}" >/dev/null 2>&1; then
echo "Trivy report is invalid JSON." >> release-notes.md
echo >> release-notes.md
continue
fi
high_critical_count="$(jq -r '[.Results[]? | (.Vulnerabilities[]?, .Packages[]?.Vulnerabilities[]?) | select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' "${report}")"
if [[ "${high_critical_count}" == "0" ]]; then
echo "No HIGH/CRITICAL vulnerabilities found." >> release-notes.md
echo >> release-notes.md
continue
fi
{
echo "| Severity | Vulnerability | Package | Installed | Fixed |"
echo "| --- | --- | --- | --- | --- |"
} >> release-notes.md
jq -r '
.Results[]?
| (.Vulnerabilities[]?, .Packages[]?.Vulnerabilities[]?)
| select(.Severity == "CRITICAL" or .Severity == "HIGH")
| [.Severity, .VulnerabilityID, .PkgName, .InstalledVersion, (.FixedVersion // "n/a")]
| @tsv
' "${report}" | awk 'NR<=20' | while IFS=$'\t' read -r severity vuln_id pkg installed fixed; do
echo "| ${severity} | ${vuln_id} | ${pkg} | ${installed} | ${fixed} |" >> release-notes.md
done
echo >> release-notes.md
done
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
shopt -s nullglob
report_files=(trivy-report-*.json)
manifest_files=(.artifacts/manifests/*.yaml)
chart_files=(.artifacts/helm/*.tgz)
attach_files=("${report_files[@]}" "${manifest_files[@]}" "${chart_files[@]}")
if gh release view "${RELEASE_TAG}" >/dev/null 2>&1; then
gh release edit "${RELEASE_TAG}" \
--title "Release ${RELEASE_TAG}" \
--notes-file release-notes.md
if (( ${#attach_files[@]} > 0 )); then
gh release upload "${RELEASE_TAG}" "${attach_files[@]}" --clobber
fi
exit 0
fi
if (( ${#attach_files[@]} > 0 )); then
gh release create "${RELEASE_TAG}" \
--title "Release ${RELEASE_TAG}" \
--notes-file release-notes.md \
"${attach_files[@]}"
exit 0
fi
gh release create "${RELEASE_TAG}" \
--title "Release ${RELEASE_TAG}" \
--notes-file release-notes.md