Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 7 additions & 89 deletions .github/actions/apply-oci-labels/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,23 @@ inputs:
required: false
default: ""
dockerfile:
description: "Path to the Dockerfile. The last FROM line drives base.name / base.digest resolution unless base-name/base-digest overrides are set."
description: "Deprecated no-op. Base image labels are intentionally not emitted by this action."
required: false
default: "./Dockerfile"
base-name:
description: "Override for org.opencontainers.image.base.name. Set this for build systems where the Dockerfile is not parseable (Family B-E)."
description: "Deprecated no-op. Base image labels are intentionally not emitted by this action."
required: false
default: ""
base-digest:
description: "Override for org.opencontainers.image.base.digest."
description: "Deprecated no-op. Base image labels are intentionally not emitted by this action."
required: false
default: ""
documentation-url:
description: "Value for image.url and image.documentation."
required: false
default: "https://documentation.suse.com/cloudnative/suse-observability/latest/en/classic.html"
registry-credentials:
description: |
Optional JSON array of registry credentials. Each entry is an object with
keys `registry`, `username`, `password`. The action runs `docker login`
for every entry before resolving base image metadata, so private-registry
bases can be inspected. Example:
[{"registry": "dp.apps.rancher.io", "username": "...", "password": "..."}]
description: "Deprecated no-op. Kept for caller compatibility."
required: false
default: "[]"

Expand All @@ -59,11 +54,6 @@ outputs:
runs:
using: composite
steps:
# Precondition: docker buildx must be set up by the caller. push-single-arch
# already does this via docker/setup-buildx-action. The action relies on
# `docker buildx imagetools inspect` to resolve the base image digest without
# pulling the image.

- name: Validate required inputs
shell: bash
env:
Expand All @@ -81,74 +71,6 @@ runs:
exit 1
fi

# `docker buildx imagetools inspect` (used to resolve base.digest below)
# hits the registry API directly and needs auth for non-public bases. We
# loop over the JSON array with --password-stdin so credentials never
# appear on the command line. Matches scan-image's bash login pattern.
- name: Login to source registries
if: ${{ inputs.registry-credentials != '[]' && inputs.registry-credentials != '' }}
shell: bash
env:
REGISTRY_CREDENTIALS: ${{ inputs.registry-credentials }}
run: |
set -euo pipefail
printf '%s' "${REGISTRY_CREDENTIALS}" | jq -c '.[]' | while IFS= read -r entry; do
registry=$(printf '%s' "${entry}" | jq -r '.registry // ""')
username=$(printf '%s' "${entry}" | jq -r '.username // ""')
password=$(printf '%s' "${entry}" | jq -r '.password // ""')
if [ -z "${registry}" ] || [ -z "${username}" ] || [ -z "${password}" ]; then
echo "::error::registry-credentials entry missing one of registry/username/password" >&2
exit 1
fi
printf '%s' "${password}" | docker login "${registry}" --username "${username}" --password-stdin
done

- name: Resolve base image
id: base
shell: bash
env:
DOCKERFILE: ${{ inputs.dockerfile }}
BASE_NAME_IN: ${{ inputs.base-name }}
BASE_DIGEST_IN: ${{ inputs.base-digest }}
run: |
set -euo pipefail

if [ -n "${BASE_NAME_IN}" ]; then
base_name="${BASE_NAME_IN}"
else
if [ ! -f "${DOCKERFILE}" ]; then
echo "::error::dockerfile not found and no base-name override given: ${DOCKERFILE}" >&2
exit 1
fi

# Export ARG defaults from the Dockerfile so envsubst can expand ${...} in FROM.
# Only ARGs with a default value (ARG NAME=VALUE) are considered.
while IFS= read -r line; do
if [[ "${line}" =~ ^[[:space:]]*ARG[[:space:]]+([A-Za-z_][A-Za-z0-9_]*)=([^[:space:]]+) ]]; then
export "${BASH_REMATCH[1]}=${BASH_REMATCH[2]}"
fi
done < "${DOCKERFILE}"

# Last FROM wins. Strip an optional --platform=... flag and a trailing `AS <stage>`.
raw=$(grep -E '^[[:space:]]*FROM[[:space:]]' "${DOCKERFILE}" | tail -n1 \
| sed -E 's/^[[:space:]]*FROM[[:space:]]+(--platform=[^[:space:]]+[[:space:]]+)?([^[:space:]]+).*/\2/')
base_name=$(envsubst <<< "${raw}")

if [ -z "${base_name}" ]; then
echo "::error::failed to resolve base image from ${DOCKERFILE}" >&2
exit 1
fi
fi

if [ -n "${BASE_DIGEST_IN}" ]; then
base_digest="${BASE_DIGEST_IN}"
else
base_digest=$(docker buildx imagetools inspect "${base_name}" --format '{{json .Manifest}}' | jq -er '.digest')
fi

echo "name=${base_name}" >> "$GITHUB_OUTPUT"
echo "digest=${base_digest}" >> "$GITHUB_OUTPUT"

- name: Compose label set
id: compose
shell: bash
Expand All @@ -160,8 +82,6 @@ runs:
COMPONENT: ${{ inputs.component }}
PRODUCT: ${{ inputs.product }}
UPSTREAM: ${{ inputs.upstream-name }}
BASE_NAME: ${{ steps.base.outputs.name }}
BASE_DIGEST: ${{ steps.base.outputs.digest }}
DOC_URL: ${{ inputs.documentation-url }}
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
Expand All @@ -182,9 +102,9 @@ runs:
run_url="${source_url}/actions/runs/${RUN_ID}"
created=$(date -u +"%Y-%m-%dT%H:%M:%SZ")

# Canonical SUSE Observability OCI label set (docker-image-oci-labels.md §4).
# Rows 15-18 (opensuse.reference, artifacthub.{logo,readme}-url,
# openbuildservice.disturl) are deferred per §8.6 and NOT emitted here.
# Canonical SUSE Observability label set. Base image labels are omitted
# deliberately: resolving them requires Dockerfile parsing and registry
# lookups that are brittle enough to block image publication.
{
echo 'labels<<__LABELS_EOF__'
echo "org.opencontainers.image.title=${TITLE}"
Expand All @@ -194,8 +114,6 @@ runs:
echo "org.opencontainers.image.source=${source_url}"
echo "org.opencontainers.image.revision=${SHA}"
echo "org.opencontainers.image.created=${created}"
echo "org.opencontainers.image.base.name=${BASE_NAME}"
echo "org.opencontainers.image.base.digest=${BASE_DIGEST}"
echo "org.opencontainers.image.vendor=SUSE LLC"
echo "org.opencontainers.image.authors=suse-observability-ops@suse.com"
echo "org.opencontainers.image.url=${DOC_URL}"
Expand Down
9 changes: 0 additions & 9 deletions .github/actions/apply-oci-labels/testdata/Dockerfile

This file was deleted.

46 changes: 11 additions & 35 deletions .github/workflows/apply-oci-labels-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,49 +21,27 @@ concurrency:

jobs:
smoke:
name: smoke (${{ matrix.case.name }} / ${{ matrix.arch }})
runs-on: ${{ matrix.runner }}
name: smoke (${{ matrix.case.name }})
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
arch: [amd64, arm64]
case:
# Exercises Dockerfile parsing + ARG substitution + imagetools digest
# resolution against a real released BCI tag. base.digest is checked
# by regex so the test stays green if SUSE re-pushes the tag.
- name: from-dockerfile
base-name: ""
base-digest: ""
- name: default-product-with-upstream
product: ""
upstream-name: TestUpstream
expect-base-name: "registry.suse.com/bci/bci-micro:15.7-56.15"
expect-product: "suse-observability"
expect-upstream: "true"
# Both overrides set: action skips Dockerfile parsing AND the network
# call. Asserts overrides win and the absent upstream-name input
# suppresses the upstream.name label entirely.
- name: with-overrides
base-name: "docker.io/library/alpine:3.20"
base-digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000"
- name: explicit-product-without-upstream
product: suse-observability-agent
upstream-name: ""
expect-base-name: "docker.io/library/alpine:3.20"
expect-product: "suse-observability-agent"
expect-upstream: "false"
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

# Action precondition (docker buildx imagetools inspect on the base image).
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0

- name: Apply OCI labels
id: apply
uses: ./.github/actions/apply-oci-labels
Expand All @@ -75,14 +53,10 @@ jobs:
component: smoke
product: ${{ matrix.case.product }}
upstream-name: ${{ matrix.case.upstream-name }}
dockerfile: .github/actions/apply-oci-labels/testdata/Dockerfile
base-name: ${{ matrix.case.base-name }}
base-digest: ${{ matrix.case.base-digest }}

- name: Assert canonical labels
env:
LABELS: ${{ steps.apply.outputs.labels }}
EXPECT_BASE_NAME: ${{ matrix.case.expect-base-name }}
EXPECT_PRODUCT: ${{ matrix.case.expect-product }}
EXPECT_UPSTREAM: ${{ matrix.case.expect-upstream }}
run: |
Expand All @@ -96,13 +70,15 @@ jobs:
grep -Fx "org.opencontainers.image.vendor=SUSE LLC" <<<"${LABELS}"
grep -Fx "org.opencontainers.image.authors=suse-observability-ops@suse.com" <<<"${LABELS}"
grep -Fx "com.suse.observability.component=smoke" <<<"${LABELS}"

# Per-case variable lines.
grep -Fx "org.opencontainers.image.base.name=${EXPECT_BASE_NAME}" <<<"${LABELS}"
grep -Fx "com.suse.observability.product=${EXPECT_PRODUCT}" <<<"${LABELS}"

# base.digest must look like sha256:<64 hex>.
grep -Eq '^org\.opencontainers\.image\.base\.digest=sha256:[a-f0-9]{64}$' <<<"${LABELS}"
# Base image labels are intentionally not emitted. Keeping this
# assertion protects the action from reintroducing Dockerfile parsing
# and registry lookups in the publish path.
if grep -Eq '^org\.opencontainers\.image\.base\.(name|digest)=' <<<"${LABELS}"; then
echo "::error::base image labels should not be emitted by apply-oci-labels" >&2
exit 1
fi

# upstream.name is emitted only when the upstream-name input is non-empty.
if [ "${EXPECT_UPSTREAM}" = "true" ]; then
Expand Down