diff --git a/system_files/bluefin/usr/libexec/bonedigger-report b/system_files/bluefin/usr/libexec/bonedigger-report index 8a8220d1..5b566d2a 100755 --- a/system_files/bluefin/usr/libexec/bonedigger-report +++ b/system_files/bluefin/usr/libexec/bonedigger-report @@ -117,27 +117,10 @@ read_boot_status() { } route_issue_repo() { - case "$IMAGE_NAME" in - bluefin-lts*) - BUG_REPO="projectbluefin/bluefin-lts" - ;; - bluefin) - if [[ "$IMAGE_TAG" == lts* ]]; then - BUG_REPO="projectbluefin/bluefin-lts" - else - BUG_REPO="projectbluefin/bluefin" - fi - ;; - bluefin*) - BUG_REPO="projectbluefin/bluefin" - ;; - dakota*) - BUG_REPO="projectbluefin/dakota" - ;; - *) - BUG_REPO="projectbluefin/common" - ;; - esac + # Routing grammar lives in one place only — see + # system_files/shared/usr/libexec/ublue-image-repo. + BUG_REPO="$("${UBLUE_IMAGE_REPO_BIN:-/usr/libexec/ublue-image-repo}" \ + --default "projectbluefin/common" "$IMAGE_NAME" "$IMAGE_TAG")" } scrub_kernel_log() { diff --git a/system_files/bluefin/usr/share/ublue-os/just/changelog.just b/system_files/bluefin/usr/share/ublue-os/just/changelog.just index 0bdb16b1..ee1647d8 100644 --- a/system_files/bluefin/usr/share/ublue-os/just/changelog.just +++ b/system_files/bluefin/usr/share/ublue-os/just/changelog.just @@ -11,15 +11,11 @@ changelogs: IMAGE_INFO_FILE="${IMAGE_INFO_FILE:-/usr/share/ublue-os/image-info.json}" TAG="$(jq -r '.["image-tag"]' < "${IMAGE_INFO_FILE}")" - # Select the correct upstream repo based on image stream + # Select the correct upstream repo based on image stream. The name/tag -> + # repo grammar is owned by /usr/libexec/ublue-image-repo; do not restate it. IMAGE_NAME="$(jq -r '.["image-name"] // empty' < "${IMAGE_INFO_FILE}")" - if [[ "$IMAGE_NAME" == "dakota" ]]; then - REPO="projectbluefin/dakota" - elif [[ "$IMAGE_NAME" =~ ^bluefin-lts ]] || [[ "$TAG" =~ ^lts ]]; then - REPO="projectbluefin/bluefin-lts" - else - REPO="projectbluefin/bluefin" - fi + REPO="$("${UBLUE_IMAGE_REPO_BIN:-/usr/libexec/ublue-image-repo}" \ + --default "projectbluefin/bluefin" "$IMAGE_NAME" "$TAG")" # If stable, gts, or lts, fetch from GitHub Releases if [[ "$TAG" =~ gts$|stable$|^lts ]]; then diff --git a/system_files/shared/usr/libexec/ublue-image-repo b/system_files/shared/usr/libexec/ublue-image-repo new file mode 100755 index 00000000..d74ff333 --- /dev/null +++ b/system_files/shared/usr/libexec/ublue-image-repo @@ -0,0 +1,70 @@ +#!/usr/bin/bash +# ublue-image-repo — canonical image-name/tag -> upstream GitHub repo resolver. +# +# Single source of truth for "which repository owns this booted image". +# Every consumer (bonedigger-report, ujust changelogs, ...) must route through +# here so the grammar cannot drift per call site. +# +# Usage: +# ublue-image-repo [--default REPO] [IMAGE_NAME] [IMAGE_TAG] +# +# IMAGE_NAME/IMAGE_TAG may also be supplied via the environment variables of +# the same name. --default sets the repo returned when nothing matches +# (callers differ: issue reporting falls back to common, changelogs to +# bluefin), and defaults to projectbluefin/common. + +set -euo pipefail + +UBLUE_IMAGE_REPO_DEFAULT="projectbluefin/common" + +while [[ $# -gt 0 ]]; do + case "$1" in + --default) + UBLUE_IMAGE_REPO_DEFAULT="${2:-$UBLUE_IMAGE_REPO_DEFAULT}" + shift 2 + ;; + --default=*) + UBLUE_IMAGE_REPO_DEFAULT="${1#--default=}" + shift + ;; + --) + shift + break + ;; + *) + break + ;; + esac +done + +IMAGE_NAME="${1-${IMAGE_NAME-}}" +IMAGE_TAG="${2-${IMAGE_TAG-}}" + +case "$IMAGE_NAME" in + bluefin-lts*) + printf '%s\n' "projectbluefin/bluefin-lts" + ;; + bluefin) + # The plain bluefin image publishes the LTS stream under lts* tags. + if [[ "$IMAGE_TAG" == lts* ]]; then + printf '%s\n' "projectbluefin/bluefin-lts" + else + printf '%s\n' "projectbluefin/bluefin" + fi + ;; + bluefin*) + printf '%s\n' "projectbluefin/bluefin" + ;; + dakota*) + printf '%s\n' "projectbluefin/dakota" + ;; + *) + # No name (or an unrecognised fork): an lts* tag still identifies the + # LTS stream, otherwise defer to the caller-supplied default. + if [[ "$IMAGE_TAG" == lts* ]]; then + printf '%s\n' "projectbluefin/bluefin-lts" + else + printf '%s\n' "$UBLUE_IMAGE_REPO_DEFAULT" + fi + ;; +esac diff --git a/tests/test_bonedigger_report.bats b/tests/test_bonedigger_report.bats index 8ed9a6e4..b7de0d7f 100755 --- a/tests/test_bonedigger_report.bats +++ b/tests/test_bonedigger_report.bats @@ -2,6 +2,7 @@ setup() { export BONEDIGGER_SCRIPT="$BATS_TEST_DIRNAME/../system_files/bluefin/usr/libexec/bonedigger-report" + export UBLUE_IMAGE_REPO_BIN="$BATS_TEST_DIRNAME/../system_files/shared/usr/libexec/ublue-image-repo" export WORKDIR="$BATS_TEST_DIRNAME/.bonedigger-report-test-${BATS_TEST_NUMBER}-${$}" export HOME="$WORKDIR/home" export XDG_STATE_HOME="$WORKDIR/state" diff --git a/tests/test_changelog.bats b/tests/test_changelog.bats index bdd4a5bb..a2821e5a 100644 --- a/tests/test_changelog.bats +++ b/tests/test_changelog.bats @@ -25,6 +25,9 @@ setup() { MOCKDIR="${WORKDIR}/bin" mkdir -p "${MOCKDIR}" + # Canonical repo-routing resolver used by the recipe under test. + export UBLUE_IMAGE_REPO_BIN="$BATS_TEST_DIRNAME/../system_files/shared/usr/libexec/ublue-image-repo" + # Capture file — curl writes each URL it receives here CURL_CALLS="${WORKDIR}/curl_calls" touch "${CURL_CALLS}" diff --git a/tests/test_image_repo.bats b/tests/test_image_repo.bats new file mode 100644 index 00000000..c55cef6c --- /dev/null +++ b/tests/test_image_repo.bats @@ -0,0 +1,93 @@ +#!/usr/bin/env bats +# Tests for /usr/libexec/ublue-image-repo — the single source of truth for +# image-name/tag -> upstream GitHub repository routing. + +setup() { + RESOLVER="$BATS_TEST_DIRNAME/../system_files/shared/usr/libexec/ublue-image-repo" + export RESOLVER +} + +@test "image-repo: bluefin latest routes to projectbluefin/bluefin" { + run "$RESOLVER" bluefin latest + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/bluefin" ] +} + +@test "image-repo: bluefin with lts tag routes to bluefin-lts" { + run "$RESOLVER" bluefin lts-20260601 + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/bluefin-lts" ] +} + +@test "image-repo: bluefin-lts-hwe routes to bluefin-lts" { + run "$RESOLVER" bluefin-lts-hwe stable + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/bluefin-lts" ] +} + +@test "image-repo: bluefin-nvidia routes to bluefin" { + run "$RESOLVER" bluefin-nvidia stable + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/bluefin" ] +} + +@test "image-repo: dakota routes to projectbluefin/dakota" { + run "$RESOLVER" dakota latest + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/dakota" ] +} + +# Regression: dakota-nvidia is a published image (docs/skills/image-registry.md). +# The changelog call site used an exact `== dakota` match and sent it to +# projectbluefin/bluefin; the resolver must treat every dakota* variant alike. +@test "image-repo: dakota-nvidia routes to projectbluefin/dakota" { + run "$RESOLVER" dakota-nvidia stable + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/dakota" ] +} + +@test "image-repo: unknown image falls back to caller default" { + run "$RESOLVER" --default projectbluefin/common knuckle latest + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/common" ] + + run "$RESOLVER" --default projectbluefin/bluefin knuckle latest + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/bluefin" ] +} + +@test "image-repo: default fallback is projectbluefin/common" { + run "$RESOLVER" knuckle latest + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/common" ] +} + +@test "image-repo: empty image name with lts tag still routes to bluefin-lts" { + run "$RESOLVER" --default projectbluefin/bluefin "" lts-hwe + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/bluefin-lts" ] +} + +@test "image-repo: --default= form is accepted" { + run "$RESOLVER" --default=projectbluefin/bluefin "" latest + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/bluefin" ] +} + +@test "image-repo: reads IMAGE_NAME/IMAGE_TAG from the environment" { + IMAGE_NAME=dakota-nvidia IMAGE_TAG=stable run "$RESOLVER" + [ "$status" -eq 0 ] + [ "$output" = "projectbluefin/dakota" ] +} + +# Structural invariant: no consumer may restate the routing grammar inline. +@test "image-repo: consumers do not hardcode upstream repo routing" { + local root="$BATS_TEST_DIRNAME/.." + for consumer in \ + "system_files/bluefin/usr/libexec/bonedigger-report" \ + "system_files/bluefin/usr/share/ublue-os/just/changelog.just" + do + run grep -c 'projectbluefin/\(bluefin-lts\|dakota\)"' "$root/$consumer" + [ "$output" = "0" ] + done +}