From 95a85b72814c9c46b72cefc237addfee4c19fe3f Mon Sep 17 00:00:00 2001 From: Jorge Castro Date: Thu, 24 Sep 2026 13:52:25 -0400 Subject: [PATCH 1/4] feat(countme): replace bluefin-countme with a daily projectbluefin-countme ping Dakota and Utah images now send at most one anonymous {"image": "/:"} per day to countme.projectbluefin.io/v1/ping. The stream comes from the booted bootc ref; other images (Bluefin Classic, LTS) exit before any network call. Timer and service follow upstream eos-phone-home; a failed send exits 0 and the next run retries. The previous opt-out files still block the service; the documented opt-out is masking the timer. Removes the weekly /metalink client (bluefin-countme) and its tests. Assisted-by: Claude Opus 5.5 via pi --- Justfile | 4 + docs/skills/image-registry.md | 47 ++-- .../system-preset/03-bluefin-countme.preset | 1 - .../03-projectbluefin-countme.preset | 1 + .../systemd/system/bluefin-countme.service | 24 -- .../lib/systemd/system/bluefin-countme.timer | 18 -- .../system/projectbluefin-countme.service | 16 ++ .../system/projectbluefin-countme.timer | 11 + .../timers.target.wants/bluefin-countme.timer | 1 - .../projectbluefin-countme.timer | 1 + .../shared/usr/libexec/bluefin-countme | 149 ----------- .../shared/usr/libexec/projectbluefin-countme | 29 +++ tests/test_bluefin_countme.bats | 240 ------------------ tests/test_projectbluefin_countme.bats | 95 +++++++ 14 files changed, 184 insertions(+), 453 deletions(-) delete mode 100644 system_files/shared/usr/lib/systemd/system-preset/03-bluefin-countme.preset create mode 100644 system_files/shared/usr/lib/systemd/system-preset/03-projectbluefin-countme.preset delete mode 100644 system_files/shared/usr/lib/systemd/system/bluefin-countme.service delete mode 100644 system_files/shared/usr/lib/systemd/system/bluefin-countme.timer create mode 100644 system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service create mode 100644 system_files/shared/usr/lib/systemd/system/projectbluefin-countme.timer delete mode 120000 system_files/shared/usr/lib/systemd/system/timers.target.wants/bluefin-countme.timer create mode 120000 system_files/shared/usr/lib/systemd/system/timers.target.wants/projectbluefin-countme.timer delete mode 100755 system_files/shared/usr/libexec/bluefin-countme create mode 100755 system_files/shared/usr/libexec/projectbluefin-countme delete mode 100755 tests/test_bluefin_countme.bats create mode 100755 tests/test_projectbluefin_countme.bats diff --git a/Justfile b/Justfile index d748acba7..ab97a1ad7 100644 --- a/Justfile +++ b/Justfile @@ -46,8 +46,12 @@ test: bats tests/test_damask_service.bats bats tests/test_suite_registration.bats bats tests/test_shared_just.bats +<<<<<<< HEAD bats tests/test_bluefin_countme.bats bats tests/test_escl_fixture.bats +======= + bats tests/test_projectbluefin_countme.bats +>>>>>>> 9cb7aeb (feat(countme): replace bluefin-countme with a daily projectbluefin-countme ping) # Preview Bazaar config from this checkout on the local machine bazaar-preview: diff --git a/docs/skills/image-registry.md b/docs/skills/image-registry.md index 4dc227dae..589422216 100644 --- a/docs/skills/image-registry.md +++ b/docs/skills/image-registry.md @@ -101,26 +101,33 @@ IMAGE_REGISTRY="ghcr.io/${IMAGE_VENDOR}" The Containerfile pulls wallpaper artwork from `ghcr.io/ublue-os/bluefin-wallpapers-gnome` as a **build-time COPY source**. This is a read-only upstream artwork dependency and does not violate the ublue-os prohibition. The production image tree and all runtime registries are fully under `ghcr.io/projectbluefin/`. See [`containerfile/SKILL.md`](containerfile/SKILL.md) for details. -## Dakota CountMe reporting - -Only Dakota currently publishes a first-party count. It ships -`/usr/libexec/bluefin-countme` and `bluefin-countme.{service,timer}` from this -repo through Dakota's pinned `elements/bluefin/common.bst`. The units require -`ID=bluefin-dakota`; the script also exits before reporting from other images. - -The timer tries on activation and once per calendar day, so an offline attempt -can retry. The script records a success in `/var/lib/bluefin-countme/lastrun` -only after `/metalink` accepts the ping and sends at most once per UTC -Monday-anchored week. It reads `:stable` or `:testing` from the booted image -ref, including tagged digest refs. The baked `latest` tag is not a stream: if -the booted ref provides no valid stream, it retries instead of reporting it. -The opt-out is `systemctl mask --now bluefin-countme.timer bluefin-countme.service`. - -`countme.projectbluefin.io/counts.json` derives Dakota's named stream counts -from its own D1 rows; other tags remain unclassified. These are estimated -check-ins without a machine identifier, not a distinct-device census. Fedora -countme and `ublue-os/countme` are not sources for Project Bluefin counts; -Bluefin Classic's upstream chart is separate. +## CountMe reporting (Dakota and Utah) + +`/usr/libexec/projectbluefin-countme` and `projectbluefin-countme.{service,timer}` +ship from `system_files/shared` to every consumer; Dakota gets them through +its pinned `elements/bluefin/common.bst`, Utah through `COMMON_IMAGE_SHA`. The +script exits before any network call unless `image-info.json`'s `image-name` +starts with `dakota` or `utah`, so Bluefin Classic and LTS never report. + +The timer and service follow upstream +[`eos-phone-home`](https://github.com/endlessm/eos-phone-home): run on +activation and three hours after each run, skip machines with an empty +`/home`. The script sends at most once per 24h (`/var/lib/projectbluefin-countme/last`), +only `{"image": "/:"}` to +`PUT countme.projectbluefin.io/v1/ping`. The stream is the booted ref's tag +from `bootc status` (`stable`, `testing`, else `unknown`), never the baked +`image-tag`. No booted bootc image means no report. A failed send exits 0 and +the next timer run retries. + +Opt out with `systemctl mask --now projectbluefin-countme.timer`. The previous +opt-out files (`/etc/projectbluefin/countme/disabled`, +`/etc/bluefin-countme-opt-out`, `/etc/dakota-countme/disabled`) still block +the service. + +`countme.projectbluefin.io/v1/daily.json` lists systems active per UTC day +and image. These are anonymous check-ins without a machine identifier, not a +device census. Fedora countme and `ublue-os/countme` are not sources for +Project Bluefin counts. ## Runtime repository selection diff --git a/system_files/shared/usr/lib/systemd/system-preset/03-bluefin-countme.preset b/system_files/shared/usr/lib/systemd/system-preset/03-bluefin-countme.preset deleted file mode 100644 index 7ca324f45..000000000 --- a/system_files/shared/usr/lib/systemd/system-preset/03-bluefin-countme.preset +++ /dev/null @@ -1 +0,0 @@ -enable bluefin-countme.timer diff --git a/system_files/shared/usr/lib/systemd/system-preset/03-projectbluefin-countme.preset b/system_files/shared/usr/lib/systemd/system-preset/03-projectbluefin-countme.preset new file mode 100644 index 000000000..575b594ed --- /dev/null +++ b/system_files/shared/usr/lib/systemd/system-preset/03-projectbluefin-countme.preset @@ -0,0 +1 @@ +enable projectbluefin-countme.timer diff --git a/system_files/shared/usr/lib/systemd/system/bluefin-countme.service b/system_files/shared/usr/lib/systemd/system/bluefin-countme.service deleted file mode 100644 index a144c0c3e..000000000 --- a/system_files/shared/usr/lib/systemd/system/bluefin-countme.service +++ /dev/null @@ -1,24 +0,0 @@ -[Unit] -Description=Project Bluefin anonymous weekly telemetry report -Documentation=https://docs.projectbluefin.io/analytics -ConditionOSRelease=ID=bluefin-dakota -After=network-online.target ublue-booted-image.service -Wants=network-online.target -ConditionPathExists=/run/ostree-booted -ConditionPathExists=!/etc/projectbluefin/countme/disabled -ConditionPathExists=!/etc/bluefin-countme-opt-out -ConditionPathExists=!/etc/dakota-countme/disabled - -[Service] -Type=oneshot -ExecStart=/usr/libexec/bluefin-countme -StateDirectory=bluefin-countme - -# Run as root (no DynamicUser) so bootc status fallback can query status if needed. -# Hardened sandbox: -PrivateTmp=yes -ProtectSystem=strict -ProtectHome=yes -NoNewPrivileges=yes -RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 -SystemCallFilter=@system-service @mount diff --git a/system_files/shared/usr/lib/systemd/system/bluefin-countme.timer b/system_files/shared/usr/lib/systemd/system/bluefin-countme.timer deleted file mode 100644 index 709b3f205..000000000 --- a/system_files/shared/usr/lib/systemd/system/bluefin-countme.timer +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=Project Bluefin weekly telemetry timer -Documentation=https://docs.projectbluefin.io/analytics -ConditionOSRelease=ID=bluefin-dakota -ConditionPathExists=/run/ostree-booted -ConditionPathExists=!/etc/projectbluefin/countme/disabled -ConditionPathExists=!/etc/bluefin-countme-opt-out -ConditionPathExists=!/etc/dakota-countme/disabled - -[Timer] -OnActiveSec=0 -OnCalendar=daily -Persistent=true -RandomizedDelaySec=1h -FixedRandomDelay=true - -[Install] -WantedBy=timers.target diff --git a/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service b/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service new file mode 100644 index 000000000..c88ebaf06 --- /dev/null +++ b/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service @@ -0,0 +1,16 @@ +[Unit] +Description=Send daily active-system ping to Project Bluefin +Documentation=https://docs.projectbluefin.io/analytics +Wants=network-online.target +After=network-online.target +ConditionPathExists=/run/ostree-booted +ConditionDirectoryNotEmpty=/home +# Opt-out files honored by the previous bluefin-countme units. +ConditionPathExists=!/etc/projectbluefin/countme/disabled +ConditionPathExists=!/etc/bluefin-countme-opt-out +ConditionPathExists=!/etc/dakota-countme/disabled + +[Service] +Type=simple +ExecStart=/usr/libexec/projectbluefin-countme +StateDirectory=projectbluefin-countme diff --git a/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.timer b/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.timer new file mode 100644 index 000000000..bb598acb1 --- /dev/null +++ b/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Send daily active-system ping to Project Bluefin +Wants=network.target + +[Timer] +OnActiveSec=0 +OnUnitInactiveSec=3h +AccuracySec=30m + +[Install] +WantedBy=timers.target diff --git a/system_files/shared/usr/lib/systemd/system/timers.target.wants/bluefin-countme.timer b/system_files/shared/usr/lib/systemd/system/timers.target.wants/bluefin-countme.timer deleted file mode 120000 index e14fc23db..000000000 --- a/system_files/shared/usr/lib/systemd/system/timers.target.wants/bluefin-countme.timer +++ /dev/null @@ -1 +0,0 @@ -../bluefin-countme.timer \ No newline at end of file diff --git a/system_files/shared/usr/lib/systemd/system/timers.target.wants/projectbluefin-countme.timer b/system_files/shared/usr/lib/systemd/system/timers.target.wants/projectbluefin-countme.timer new file mode 120000 index 000000000..66e3d1e6f --- /dev/null +++ b/system_files/shared/usr/lib/systemd/system/timers.target.wants/projectbluefin-countme.timer @@ -0,0 +1 @@ +../projectbluefin-countme.timer \ No newline at end of file diff --git a/system_files/shared/usr/libexec/bluefin-countme b/system_files/shared/usr/libexec/bluefin-countme deleted file mode 100755 index 4cecff232..000000000 --- a/system_files/shared/usr/libexec/bluefin-countme +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env bash -# /usr/libexec/bluefin-countme -# Weekly anonymous countme reporting service for Dakota images -set -euo pipefail - -IMAGE_INFO="${IMAGE_INFO_FILE:-/usr/share/ublue-os/image-info.json}" -STATE_DIR="${STATE_DIRECTORY:-/var/lib/bluefin-countme}" -BOOTED_IMAGE_FILE="${BOOTED_IMAGE_FILE:-/run/ublue-os/booted-image}" -NOW=$(date +%s) -WEEK=604800 -# Unix epoch was Thursday; adding three days aligns week numbers to UTC Monday. -UTC_WEEK=$(( (NOW + 259200) / WEEK )) - -# 1. Opt-out checks -if [[ -f /etc/projectbluefin/countme/disabled ]] || \ - [[ -f /etc/bluefin-countme-opt-out ]] || \ - [[ -f /etc/dakota-countme/disabled ]]; then - exit 0 -fi - -# 2. Image scope: track only Dakota images, labeled as "Bluefin". -# Non-Dakota systems exit 0 immediately without touching state or making network calls. -if [[ ! -f "$IMAGE_INFO" ]]; then - exit 0 -fi -raw_name=$(jq -r '."image-name" // empty' "$IMAGE_INFO" 2>/dev/null || true) -if [[ -z "$raw_name" ]] || [[ "$raw_name" != dakota* ]]; then - exit 0 -fi - -# Ensure state directory exists -mkdir -p "$STATE_DIR" - -EPOCH_FILE="${STATE_DIR}/epoch" -LASTRUN_FILE="${STATE_DIR}/lastrun" - -# 3. Initialize epoch file on first run or recover if corrupt -if [[ -f "$EPOCH_FILE" ]]; then - EPOCH=$(< "$EPOCH_FILE") - if ! [[ "$EPOCH" =~ ^[0-9]+$ ]]; then - echo "$NOW" > "$EPOCH_FILE" - EPOCH="$NOW" - fi -else - echo "$NOW" > "$EPOCH_FILE" - EPOCH="$NOW" -fi - -# 4. Send at most one successful report per UTC week, even with daily retries. -if [[ -f "$LASTRUN_FILE" ]]; then - LASTRUN=$(< "$LASTRUN_FILE") - if [[ "$LASTRUN" =~ ^[0-9]+$ ]] && (( (LASTRUN + 259200) / WEEK == UTC_WEEK )); then - exit 0 - fi -fi - -# 5. Compute Fedora-style week bucket -WEEKS=$(( (NOW - EPOCH) / WEEK + 1 )) -if (( WEEKS == 1 )); then - BUCKET=1 # First week -elif (( WEEKS >= 2 && WEEKS <= 4 )); then - BUCKET=2 # First month (weeks 2-4) -elif (( WEEKS >= 5 && WEEKS <= 24 )); then - BUCKET=3 # First 6 months (weeks 5-24) -else - BUCKET=4 # >6 months -fi - -# 6. Extract image metadata -REPO="dakota" -TAG="" -FLAVOR="main" - -raw_flavor=$(jq -r '."image-flavor" // empty' "$IMAGE_INFO" 2>/dev/null || true) -if [[ -n "$raw_flavor" ]]; then - FLAVOR="$raw_flavor" -fi - -raw_tag=$(jq -r '."image-tag" // empty' "$IMAGE_INFO" 2>/dev/null || true) -if [[ -n "$raw_tag" ]]; then - TAG="$raw_tag" -fi - -# 7. Extract true runtime tag (:stable vs :testing) from booted deployment -# Avoid compose-time defaults; inspect /run/ublue-os/booted-image or bootc status -booted_ref="" -if [[ -r "$BOOTED_IMAGE_FILE" ]]; then - read -r booted_ref < "$BOOTED_IMAGE_FILE" || true -fi - -if [[ -z "$booted_ref" ]] && command -v bootc &>/dev/null; then - bootc_stderr=$(mktemp) - if bootc_out=$(bootc status --json 2>"$bootc_stderr"); then - booted_ref=$(printf '%s' "$bootc_out" | jq -r '.status.booted.image.image.image // .spec.image.image // empty' 2>/dev/null || true) - else - echo "bootc status failed: $(< "$bootc_stderr")" >&2 - fi - rm -f "$bootc_stderr" -fi - -if [[ -n "$booted_ref" ]]; then - booted_image="${booted_ref%%@*}" - booted_image="${booted_image##*/}" - if [[ "$booted_image" == *:* ]]; then - TAG="${booted_image##*:}" - fi -fi - -# Normalize tag to primary streams if matching standard aliases -case "$TAG" in - stable*|gts*|lts) TAG="stable" ;; - testing*|canary*|next*) TAG="testing" ;; -esac -if [[ "$TAG" != "stable" && "$TAG" != "testing" ]]; then - echo "Booted stream unavailable; countme will retry" >&2 - exit 1 -fi - -# 8. Detect Game Mode status (1 if gamescope/steam session or gaming flavor is present) -GAMEMODE=0 -WAYLAND_SESSIONS_DIR="${WAYLAND_SESSIONS_DIR:-/usr/share/wayland-sessions}" -if [[ "$FLAVOR" =~ gaming ]] || [[ "${raw_name:-}" =~ gaming ]]; then - GAMEMODE=1 -elif [[ -f "${WAYLAND_SESSIONS_DIR}/gamescope-session-steam.desktop" ]] || \ - [[ -f "${WAYLAND_SESSIONS_DIR}/gamescope-session.desktop" ]] || \ - command -v gamescope-session &>/dev/null; then - GAMEMODE=1 -fi - -ARCH=$(uname -m) -OS_VERSION=$(grep '^VERSION_ID=' /etc/os-release 2>/dev/null | cut -d= -f2 | tr -d '"' || echo "unknown") - -# 9. Query countme endpoint using curl -G --data-urlencode -USER_AGENT="bluefin-countme (${REPO} ${OS_VERSION}; ${FLAVOR}; tag=${TAG}; gamemode=${GAMEMODE}; Linux.${ARCH})" -ENDPOINT="https://countme.projectbluefin.io/metalink" - -QUERY_ARGS=( - --data-urlencode "repo=${REPO}" - --data-urlencode "tag=${TAG}" - --data-urlencode "flavor=${FLAVOR}" - --data-urlencode "gamemode=${GAMEMODE}" - --data-urlencode "arch=${ARCH}" - --data-urlencode "countme=${BUCKET}" -) - -curl -sfG --retry 3 --max-time 15 -H "User-Agent: ${USER_AGENT}" \ - "${QUERY_ARGS[@]}" \ - "$ENDPOINT" -o /dev/null -echo "$NOW" > "$LASTRUN_FILE" diff --git a/system_files/shared/usr/libexec/projectbluefin-countme b/system_files/shared/usr/libexec/projectbluefin-countme new file mode 100755 index 000000000..30fd3c9c9 --- /dev/null +++ b/system_files/shared/usr/libexec/projectbluefin-countme @@ -0,0 +1,29 @@ +#!/usr/bin/bash +# /usr/libexec/projectbluefin-countme +# Anonymous daily active-system count for Dakota and Utah images. Sends only +# "/:", at most once per day, to +# countme.projectbluefin.io (docs.projectbluefin.io/analytics). +# Opt out: systemctl mask --now projectbluefin-countme.timer +set -uo pipefail + +info="${IMAGE_INFO:-/usr/share/ublue-os/image-info.json}" +last="${STATE_DIRECTORY:-/var/lib/projectbluefin-countme}/last" + +# Only images the service counts; everything else never touches the network. +name=$(jq -r '."image-name" // empty' "$info" 2>/dev/null) +[[ $name == dakota* || $name == utah* ]] || exit 0 + +[[ -e $last ]] && (( $(date +%s) - $(stat -c %Y "$last") < 86400 )) && exit 0 + +# No booted bootc image (live ISO, installer, container): nothing to count. +ref=$(bootc status --json 2>/dev/null | jq -r '.status.booted.image.image.image // empty' 2>/dev/null) +[[ -n $ref ]] || exit 0 +ref=${ref%%@*} +tag=${ref##*:} +[[ $tag == stable || $tag == testing ]] || tag=unknown +image="$name/$(jq -r '."image-flavor"' "$info"):$tag" + +# Like eos-phone-home, a failed send exits 0; the timer tries again later. +curl -fsS --max-time 30 -X PUT --json "$(jq -nc --arg i "$image" '{image: $i}')" \ + "${COUNTME_HOST:-https://countme.projectbluefin.io}/v1/ping" >/dev/null && touch "$last" +exit 0 diff --git a/tests/test_bluefin_countme.bats b/tests/test_bluefin_countme.bats deleted file mode 100755 index 1a805ae9c..000000000 --- a/tests/test_bluefin_countme.bats +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/env bats - -setup() { - export TEST_TMPDIR="$(mktemp -d)" - export STATE_DIRECTORY="${TEST_TMPDIR}/state" - export IMAGE_INFO_FILE="${TEST_TMPDIR}/image-info.json" - export BOOTED_IMAGE_FILE="${TEST_TMPDIR}/booted-image" - export WAYLAND_SESSIONS_DIR="${TEST_TMPDIR}/wayland-sessions" - export MOCK_BIN="${TEST_TMPDIR}/bin" - mkdir -p "${STATE_DIRECTORY}" "${MOCK_BIN}" "${WAYLAND_SESSIONS_DIR}" - - # Mock curl to inspect executed args/URL - cat << 'CURL_EOF' > "${MOCK_BIN}/curl" -#!/usr/bin/env bash -echo "$@" >> "${STATE_DIRECTORY}/curl_calls.log" -exit 0 -CURL_EOF - chmod +x "${MOCK_BIN}/curl" - - # Mock bootc to prevent real host bootc from interfering - cat << 'BOOTC_EOF' > "${MOCK_BIN}/bootc" -#!/usr/bin/env bash -exit 1 -BOOTC_EOF - chmod +x "${MOCK_BIN}/bootc" - - export PATH="${MOCK_BIN}:${PATH}" -} - -teardown() { - rm -rf "${TEST_TMPDIR}" -} -set_fake_now() { - export COUNTME_TEST_NOW="$1" - cat << 'DATE_EOF' > "${MOCK_BIN}/date" -#!/usr/bin/env bash -printf '%s\n' "$COUNTME_TEST_NOW" -DATE_EOF - chmod +x "${MOCK_BIN}/date" -} - - -@test "bluefin-countme generates epoch and sends countme=1 on first run for dakota" { - cat << 'EOF' > "${IMAGE_INFO_FILE}" -{ - "image-name": "dakota", - "image-flavor": "main", - "image-tag": "stable" -} -EOF - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 0 ] - - [ -f "${STATE_DIRECTORY}/epoch" ] - [ -f "${STATE_DIRECTORY}/lastrun" ] - - run cat "${STATE_DIRECTORY}/curl_calls.log" - [ "$status" -eq 0 ] - [[ "$output" =~ "repo=dakota" ]] - [[ "$output" =~ "tag=stable" ]] - [[ "$output" =~ "flavor=main" ]] - [[ "$output" =~ "gamemode=0" ]] - [[ "$output" =~ "countme=1" ]] -} - -@test "bluefin-countme detects testing tag and gaming mode" { - cat << 'EOF' > "${IMAGE_INFO_FILE}" -{ - "image-name": "dakota-gaming", - "image-flavor": "gaming", - "image-tag": "testing" -} -EOF - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 0 ] - - run cat "${STATE_DIRECTORY}/curl_calls.log" - [ "$status" -eq 0 ] - [[ "$output" =~ "repo=dakota" ]] - [[ "$output" =~ "tag=testing" ]] - [[ "$output" =~ "flavor=gaming" ]] - [[ "$output" =~ "gamemode=1" ]] -} - -@test "bluefin-countme throttles execution within weekly window" { - cat << 'EOF' > "${IMAGE_INFO_FILE}" -{ - "image-name": "dakota", - "image-flavor": "main", - "image-tag": "stable" -} -EOF - - # First run - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 0 ] - [ -f "${STATE_DIRECTORY}/curl_calls.log" ] - calls_1=$(wc -l < "${STATE_DIRECTORY}/curl_calls.log") - - # Immediate second run (within throttle window) - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 0 ] - calls_2=$(wc -l < "${STATE_DIRECTORY}/curl_calls.log") - - [ "$calls_1" -eq "$calls_2" ] -} - -@test "a successful ping cannot repeat within the same UTC week" { - printf '{"image-name":"dakota","image-tag":"stable"}\n' > "${IMAGE_INFO_FILE}" - last=$(date -u -d '2026-09-21T00:00:00Z' +%s) - now=$(date -u -d '2026-09-27T12:00:00Z' +%s) - printf '%s\n' "$last" > "${STATE_DIRECTORY}/lastrun" - set_fake_now "$now" - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 0 ] - [ ! -f "${STATE_DIRECTORY}/curl_calls.log" ] -} - -@test "a new UTC week can report even after a recent ping" { - printf '{"image-name":"dakota","image-tag":"testing"}\n' > "${IMAGE_INFO_FILE}" - last=$(date -u -d '2026-09-27T23:30:00Z' +%s) - now=$(date -u -d '2026-09-28T00:15:00Z' +%s) - printf '%s\n' "$last" > "${STATE_DIRECTORY}/lastrun" - set_fake_now "$now" - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 0 ] - [ -f "${STATE_DIRECTORY}/curl_calls.log" ] - [ "$(< "${STATE_DIRECTORY}/lastrun")" -eq "$now" ] -} - -@test "a failed upload returns failure and leaves this week retryable" { - printf '{"image-name":"dakota","image-tag":"testing"}\n' > "${IMAGE_INFO_FILE}" - printf '#!/usr/bin/env bash\nexit 22\n' > "${MOCK_BIN}/curl" - chmod +x "${MOCK_BIN}/curl" - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 22 ] - [ ! -f "${STATE_DIRECTORY}/lastrun" ] -} - -@test "bluefin-countme calculates bucket based on epoch age" { - cat << 'EOF' > "${IMAGE_INFO_FILE}" -{ - "image-name": "dakota", - "image-flavor": "main", - "image-tag": "stable" -} -EOF - # Set epoch to 3 weeks ago (bucket 2) - now=$(date +%s) - three_weeks_ago=$(( now - (3 * 7 * 86400) )) - echo "$three_weeks_ago" > "${STATE_DIRECTORY}/epoch" - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 0 ] - - run cat "${STATE_DIRECTORY}/curl_calls.log" - [ "$status" -eq 0 ] - [[ "$output" =~ "countme=2" ]] -} - -@test "bluefin-countme exits 0 without calling countme endpoint on non-dakota images" { - cat << 'EOF' > "${IMAGE_INFO_FILE}" -{ - "image-name": "bluefin-lts", - "image-flavor": "main", - "image-tag": "stable" -} -EOF - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 0 ] - [ ! -f "${STATE_DIRECTORY}/curl_calls.log" ] -} - -@test "bluefin-countme extracts runtime tag from bootc mock" { - cat << 'EOF' > "${IMAGE_INFO_FILE}" -{ - "image-name": "dakota", - "image-flavor": "main", - "image-tag": "stable" -} -EOF - - cat << 'BOOTC_EOF' > "${MOCK_BIN}/bootc" -#!/usr/bin/env bash -cat << 'JSON' -{ - "status": { - "booted": { - "image": { - "image": { - "image": "ghcr.io/projectbluefin/dakota:testing" - } - } - } - } -} -JSON -BOOTC_EOF - chmod +x "${MOCK_BIN}/bootc" - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 0 ] - - run cat "${STATE_DIRECTORY}/curl_calls.log" - [ "$status" -eq 0 ] - [[ "$output" =~ "tag=testing" ]] -} -@test "a missing booted stream does not invent a latest count" { - printf '{"image-name":"dakota","image-tag":"latest"}\n' > "${IMAGE_INFO_FILE}" - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -ne 0 ] - [ ! -f "${STATE_DIRECTORY}/curl_calls.log" ] - [ ! -f "${STATE_DIRECTORY}/lastrun" ] -} - -@test "missing image and booted tags cannot invent stable check-ins" { - printf '{"image-name":"dakota"}\n' > "${IMAGE_INFO_FILE}" - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -ne 0 ] - [ ! -f "${STATE_DIRECTORY}/curl_calls.log" ] - [ ! -f "${STATE_DIRECTORY}/lastrun" ] -} - -@test "digest-pinned booted ref retains its testing tag" { - printf '{"image-name":"dakota","image-tag":"latest"}\n' > "${IMAGE_INFO_FILE}" - printf 'ghcr.io/projectbluefin/dakota:testing@sha256:abc123\n' > "${BOOTED_IMAGE_FILE}" - - run bash system_files/shared/usr/libexec/bluefin-countme - [ "$status" -eq 0 ] - run cat "${STATE_DIRECTORY}/curl_calls.log" - [[ "$output" =~ "tag=testing" ]] -} diff --git a/tests/test_projectbluefin_countme.bats b/tests/test_projectbluefin_countme.bats new file mode 100755 index 000000000..d4ab40c92 --- /dev/null +++ b/tests/test_projectbluefin_countme.bats @@ -0,0 +1,95 @@ +#!/usr/bin/env bats +# /usr/libexec/projectbluefin-countme: scope, stream, daily limit, retry. + +SCRIPT="${BATS_TEST_DIRNAME}/../system_files/shared/usr/libexec/projectbluefin-countme" + +setup() { + export TEST_TMPDIR="$(mktemp -d)" + export STATE_DIRECTORY="${TEST_TMPDIR}/state" + export IMAGE_INFO="${TEST_TMPDIR}/image-info.json" + export MOCK_BIN="${TEST_TMPDIR}/bin" + mkdir -p "${STATE_DIRECTORY}" "${MOCK_BIN}" + + # curl records its arguments; CURL_EXIT simulates a failed send. + cat << 'EOF' > "${MOCK_BIN}/curl" +#!/usr/bin/env bash +echo "$@" >> "${TEST_TMPDIR}/curl.log" +exit "${CURL_EXIT:-0}" +EOF + # bootc prints BOOTED_REF as the booted image, or fails when it is unset. + cat << 'EOF' > "${MOCK_BIN}/bootc" +#!/usr/bin/env bash +[[ -n "${BOOTED_REF:-}" ]] || exit 1 +printf '{"status":{"booted":{"image":{"image":{"image":"%s"}}}}}\n' "${BOOTED_REF}" +EOF + chmod +x "${MOCK_BIN}/curl" "${MOCK_BIN}/bootc" + export PATH="${MOCK_BIN}:${PATH}" +} + +teardown() { + rm -rf "${TEST_TMPDIR}" +} + +image_info() { + printf '{"image-name":"%s","image-flavor":"%s","image-tag":"latest"}\n' "$1" "$2" > "${IMAGE_INFO}" +} + +sent_image() { + grep -o '"image":"[^"]*"' "${TEST_TMPDIR}/curl.log" +} + +@test "Dakota reports image, flavor, and the booted stream" { + image_info dakota-nvidia nvidia + BOOTED_REF=ghcr.io/projectbluefin/dakota-nvidia:testing run "${SCRIPT}" + [ "$status" -eq 0 ] + [ "$(sent_image)" = '"image":"dakota-nvidia/nvidia:testing"' ] + grep -q '/v1/ping' "${TEST_TMPDIR}/curl.log" + [ -e "${STATE_DIRECTORY}/last" ] +} + +@test "Utah is counted; a digest-pinned ref keeps its stream" { + image_info utah gaming + BOOTED_REF='ghcr.io/projectbluefin/utah-gaming:stable@sha256:abc' run "${SCRIPT}" + [ "$(sent_image)" = '"image":"utah/gaming:stable"' ] +} + +@test "a tag that is not a stream is reported as unknown" { + image_info dakota main + BOOTED_REF=ghcr.io/projectbluefin/dakota:latest run "${SCRIPT}" + [ "$(sent_image)" = '"image":"dakota/main:unknown"' ] +} + +@test "Bluefin Classic and LTS never contact the service" { + for name in bluefin bluefin-lts; do + image_info "$name" main + BOOTED_REF="ghcr.io/ublue-os/${name}:stable" run "${SCRIPT}" + [ "$status" -eq 0 ] + done + [ ! -e "${TEST_TMPDIR}/curl.log" ] + [ ! -e "${STATE_DIRECTORY}/last" ] +} + +@test "no booted bootc image (live ISO, installer) sends nothing" { + image_info dakota main + run "${SCRIPT}" + [ "$status" -eq 0 ] + [ ! -e "${TEST_TMPDIR}/curl.log" ] +} + +@test "at most one report per 24 hours" { + image_info dakota main + touch -d '2 hours ago' "${STATE_DIRECTORY}/last" + BOOTED_REF=ghcr.io/projectbluefin/dakota:stable run "${SCRIPT}" + [ ! -e "${TEST_TMPDIR}/curl.log" ] + + touch -d '25 hours ago' "${STATE_DIRECTORY}/last" + BOOTED_REF=ghcr.io/projectbluefin/dakota:stable run "${SCRIPT}" + [ -e "${TEST_TMPDIR}/curl.log" ] +} + +@test "a failed send exits 0 and is retried on the next run" { + image_info dakota main + CURL_EXIT=22 BOOTED_REF=ghcr.io/projectbluefin/dakota:stable run "${SCRIPT}" + [ "$status" -eq 0 ] + [ ! -e "${STATE_DIRECTORY}/last" ] +} From 793cf340518883b39c71ab44adadc73e53b40b57 Mon Sep 17 00:00:00 2001 From: castrojo Date: Fri, 25 Sep 2026 19:16:17 -0400 Subject: [PATCH 2/4] fix(countme): restore service sandbox, fallback flavor to main, wire unit tests - restore systemd security hardening directives on projectbluefin-countme.service - drop vestigial Wants=network.target on timer unit - default image-flavor to 'main' when empty or null - register test_projectbluefin_countme.bats in unit-tests.yml --- .github/workflows/unit-tests.yml | 3 +++ .../lib/systemd/system/projectbluefin-countme.service | 9 +++++++++ .../usr/lib/systemd/system/projectbluefin-countme.timer | 1 - system_files/shared/usr/libexec/projectbluefin-countme | 2 +- 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 357769c12..8375ef89e 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -163,6 +163,9 @@ jobs: - name: Run bats (bonedigger report) run: bats tests/test_bonedigger_report.bats + - name: Run bats (projectbluefin-countme) + run: bats tests/test_projectbluefin_countme.bats + - name: Run bats (apps.just — install-opentabletdriver, cncf) run: bats tests/test_apps_just.bats - name: Run bats (escl-fixture) diff --git a/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service b/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service index c88ebaf06..6e4c6a0c9 100644 --- a/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service +++ b/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service @@ -14,3 +14,12 @@ ConditionPathExists=!/etc/dakota-countme/disabled Type=simple ExecStart=/usr/libexec/projectbluefin-countme StateDirectory=projectbluefin-countme + +# Run as root (no DynamicUser) so bootc status can query status. +# Hardened sandbox: +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes +NoNewPrivileges=yes +RestrictAddressFamilies=AF_UNIX AF_NETLINK AF_INET AF_INET6 +SystemCallFilter=@system-service @mount diff --git a/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.timer b/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.timer index bb598acb1..545873929 100644 --- a/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.timer +++ b/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.timer @@ -1,6 +1,5 @@ [Unit] Description=Send daily active-system ping to Project Bluefin -Wants=network.target [Timer] OnActiveSec=0 diff --git a/system_files/shared/usr/libexec/projectbluefin-countme b/system_files/shared/usr/libexec/projectbluefin-countme index 30fd3c9c9..4f9c93e09 100755 --- a/system_files/shared/usr/libexec/projectbluefin-countme +++ b/system_files/shared/usr/libexec/projectbluefin-countme @@ -21,7 +21,7 @@ ref=$(bootc status --json 2>/dev/null | jq -r '.status.booted.image.image.image ref=${ref%%@*} tag=${ref##*:} [[ $tag == stable || $tag == testing ]] || tag=unknown -image="$name/$(jq -r '."image-flavor"' "$info"):$tag" +image="$name/$(jq -r '."image-flavor" // "main"' "$info"):$tag" # Like eos-phone-home, a failed send exits 0; the timer tries again later. curl -fsS --max-time 30 -X PUT --json "$(jq -nc --arg i "$image" '{image: $i}')" \ From bc5864c477e9ff792e4e2d7aa49cb5ea2510651e Mon Sep 17 00:00:00 2001 From: castrojo Date: Fri, 25 Sep 2026 19:37:06 -0400 Subject: [PATCH 3/4] fix(ci): remove stale test_bluefin_countme.bats registration --- .github/workflows/unit-tests.yml | 3 --- Justfile | 5 ----- 2 files changed, 8 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 8375ef89e..95b719e48 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -176,9 +176,6 @@ jobs: - name: Run bats (bling preexec re-arm) run: bats tests/test_bling_preexec_rearm.bats - - name: Run bats (bluefin-countme) - run: bats tests/test_bluefin_countme.bats - - name: Run bats (brew tap trust) run: bats tests/test_brew_tap_trust.bats diff --git a/Justfile b/Justfile index ab97a1ad7..f54dac393 100644 --- a/Justfile +++ b/Justfile @@ -11,7 +11,6 @@ test: bats tests/test_privileged_setup.bats bats tests/test_bling.bats bats tests/test_bling_sh.bats - bats tests/test_bluefin_countme.bats bats tests/test_bling_preexec_rearm.bats bats tests/test_luks_tpm2.bats bats tests/test_rechunker_group_fix.bats @@ -46,12 +45,8 @@ test: bats tests/test_damask_service.bats bats tests/test_suite_registration.bats bats tests/test_shared_just.bats -<<<<<<< HEAD - bats tests/test_bluefin_countme.bats bats tests/test_escl_fixture.bats -======= bats tests/test_projectbluefin_countme.bats ->>>>>>> 9cb7aeb (feat(countme): replace bluefin-countme with a daily projectbluefin-countme ping) # Preview Bazaar config from this checkout on the local machine bazaar-preview: From 138e16e56a1426b48af720c4a54eeee958f52ce6 Mon Sep 17 00:00:00 2001 From: castrojo Date: Sat, 26 Sep 2026 16:20:43 -0400 Subject: [PATCH 4/4] fix(countme): keep honoring a mask of the old bluefin-countme units docs.projectbluefin.io/analytics tells users to opt out with `systemctl mask --now bluefin-countme.timer bluefin-countme.service`. The replacement units have new names, so those masks no longer applied and opted-out Dakota systems would start reporting again. Skip the service when either old unit is masked in /etc/systemd/system. Assisted-by: Claude via omp Signed-off-by: castrojo --- docs/skills/image-registry.md | 6 +++--- .../usr/lib/systemd/system/projectbluefin-countme.service | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/skills/image-registry.md b/docs/skills/image-registry.md index 589422216..a5d5dadc0 100644 --- a/docs/skills/image-registry.md +++ b/docs/skills/image-registry.md @@ -120,9 +120,9 @@ from `bootc status` (`stable`, `testing`, else `unknown`), never the baked the next timer run retries. Opt out with `systemctl mask --now projectbluefin-countme.timer`. The previous -opt-out files (`/etc/projectbluefin/countme/disabled`, -`/etc/bluefin-countme-opt-out`, `/etc/dakota-countme/disabled`) still block -the service. +opt-outs still block the service: the files `/etc/projectbluefin/countme/disabled`, +`/etc/bluefin-countme-opt-out`, `/etc/dakota-countme/disabled`, and a +`systemctl mask` of `bluefin-countme.timer` or `bluefin-countme.service`. `countme.projectbluefin.io/v1/daily.json` lists systems active per UTC day and image. These are anonymous check-ins without a machine identifier, not a diff --git a/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service b/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service index 6e4c6a0c9..feb190509 100644 --- a/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service +++ b/system_files/shared/usr/lib/systemd/system/projectbluefin-countme.service @@ -9,6 +9,9 @@ ConditionDirectoryNotEmpty=/home ConditionPathExists=!/etc/projectbluefin/countme/disabled ConditionPathExists=!/etc/bluefin-countme-opt-out ConditionPathExists=!/etc/dakota-countme/disabled +# `systemctl mask` of the previous units, the opt-out documented for them. +ConditionPathExists=!/etc/systemd/system/bluefin-countme.timer +ConditionPathExists=!/etc/systemd/system/bluefin-countme.service [Service] Type=simple