bread-ecosystem/scripts/doctor-channels.sh
Breadway a4f0c96b90 scripts: add onboard-product.sh, teach doctor-channels.sh to check signing secrets
breadcast shipped with a full bakery.toml + CI workflows but was missing
from registry/bread-ecosystem.toml and had zero Forgejo Actions secrets
configured, so its release workflows would have failed closed (or worse,
published unsigned on an older workflow shape) the first time they ran.
Neither gap was visible until checked by hand.

doctor-channels.sh now also flags any registry product's repo missing the
BAKERY_MINISIGN_SEC_KEY_PATH secret (soft-skipped without a local Forgejo
token). onboard-product.sh handles the one genuine write step — adding a
[[products]] entry — then runs doctor-channels.sh so nothing else gets
missed silently again. Also fixes a pre-existing false positive where the
local-checkout drift scan didn't recognize worktree checkouts of
bread-ecosystem itself beyond the one literal "-fix-worktree" suffix it
special-cased.
2026-08-05 18:54:25 +08:00

196 lines
7.1 KiB
Bash
Executable file

#!/usr/bin/env bash
# doctor-channels.sh — detect drift between a repo's declared distribution
# channel(s) and its actual .forgejo/workflows/ + packaging metadata.
#
# See docs/release-channels.md for the policy this checks against.
#
# Usage:
# scripts/doctor-channels.sh [BASE_DIR]
#
# BASE_DIR defaults to the parent of this repo checkout (i.e. run from a
# normal ~/Projects/bread-ecosystem checkout, it scans sibling ~/Projects/*
# repos). Point it at a directory of worktrees (e.g. ~/Projects, which is
# also where *-fix-worktree checkouts live) to check those instead:
#
# scripts/doctor-channels.sh ~/Projects
#
# Also checks every registry product's Forgejo repo for the
# BAKERY_MINISIGN_SEC_KEY_PATH Actions secret (via the Forgejo API) — a
# split-out product repo silently missing this secret is exactly the gotcha
# that bit breadcast's onboarding: its release workflows would either fail
# the hard-fail guard (dev/rc-style workflows) or, worse, publish unsigned
# (older release.yml-style workflows without that guard). Skipped with a
# warning (not a failure) if ~/.config/forgejo/token doesn't exist, so this
# still runs for anyone without API access — set SKIP_SECRETS_CHECK=1 to
# skip it deliberately (e.g. offline).
#
# Exits 0 if no drift found, 1 if any repo has drift (so it's CI-friendly).
#
# Requires: python3 (tomllib, stdlib since 3.11); curl + a Forgejo token at
# ~/.config/forgejo/token for the secrets check (soft-skipped without one).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BASE_DIR="${1:-$(dirname "${SCRIPT_DIR}")}"
REGISTRY="${SCRIPT_DIR}/registry/bread-ecosystem.toml"
if [[ ! -f "${REGISTRY}" ]]; then
echo "error: registry not found at ${REGISTRY}" >&2
exit 2
fi
# repo (last path segment of registry `repo = "Breadway/x"`) -> 1
mapfile -t registry_repos < <(python3 -c "
import tomllib
with open('${REGISTRY}', 'rb') as f:
d = tomllib.load(f)
for p in d['products']:
print(p['repo'].split('/')[-1])
")
is_in_registry() {
local name="$1"
for r in "${registry_repos[@]}"; do
[[ "${r}" == "${name}" ]] && return 0
done
return 1
}
# Repos with a deliberately non-standard packaging shape that the
# single-PKGBUILD/single-package.yml heuristic below doesn't fit. Extend
# this if another repo grows a legitimately special-cased layout.
PACKAGE_CHECK_EXEMPT=("bos") # ships an ISO via release-iso.yml; its PKGBUILDs
# under packaging/*/ build bundled AUR deps
# (bibata, calamares, ...), each with its own
# dedicated workflow — not a pacman-channel package.
is_package_check_exempt() {
local name="$1"
for r in "${PACKAGE_CHECK_EXEMPT[@]}"; do
[[ "${r}" == "${name}" ]] && return 0
done
return 1
}
drift=0
checked=0
for dir in "${BASE_DIR}"/*/; do
name="$(basename "${dir}")"
name="${name%-fix-worktree}" # normalize worktree checkouts back to the repo name
[[ -d "${dir}/.git" || -f "${dir}/.git" ]] || continue
# Skip bread-ecosystem itself — it's a multi-product repo the registry
# membership check above doesn't map 1:1, and it's already reviewed by
# hand above (bakery + bread-theme products).
# Prefix match, not exact: bread-ecosystem-breadcast, bread-ecosystem-onboard,
# etc. are all worktree checkouts of this same multi-product repo, not
# separate products the registry-membership check below applies to.
[[ "${name}" == bread-ecosystem* ]] && continue
checked=$((checked + 1))
has_bakery_toml=0
[[ -f "${dir}/bakery.toml" ]] && has_bakery_toml=1
has_release_wf=0
compgen -G "${dir}/.forgejo/workflows/release*.yml" >/dev/null 2>&1 && has_release_wf=1
in_registry=0
is_in_registry "${name}" && in_registry=1
has_pkgbuild=0
find "${dir}" -maxdepth 3 -iname 'PKGBUILD' -not -path '*/.git/*' 2>/dev/null \
| grep -q . && has_pkgbuild=1
has_package_wf=0
[[ -f "${dir}/.forgejo/workflows/package.yml" ]] && has_package_wf=1
issues=()
if [[ "${has_bakery_toml}" == 1 && "${in_registry}" == 0 ]]; then
issues+=("has bakery.toml but no registry/bread-ecosystem.toml entry")
fi
if [[ "${in_registry}" == 1 && "${has_bakery_toml}" == 0 ]]; then
issues+=("registered in bread-ecosystem.toml but has no bakery.toml")
fi
if [[ "${in_registry}" == 1 && "${has_release_wf}" == 0 ]]; then
issues+=("registered + has bakery.toml but no release*.yml workflow")
fi
if [[ "${has_bakery_toml}" == 1 && "${in_registry}" == 0 && "${has_release_wf}" == 1 ]]; then
issues+=("has a release workflow for a product not in the registry (index.json will never include it)")
fi
if ! is_package_check_exempt "${name}"; then
if [[ "${has_pkgbuild}" == 1 && "${has_package_wf}" == 0 ]]; then
issues+=("has a PKGBUILD but no package.yml workflow")
fi
if [[ "${has_package_wf}" == 1 && "${has_pkgbuild}" == 0 ]]; then
issues+=("has package.yml but no PKGBUILD")
fi
fi
if [[ ${#issues[@]} -gt 0 ]]; then
drift=1
echo "${name}:"
for i in "${issues[@]}"; do
echo " - ${i}"
done
fi
done
echo
echo "checked ${checked} repos under ${BASE_DIR}"
# --- Secrets check ---------------------------------------------------------
TOKEN_FILE="${HOME}/.config/forgejo/token"
if [[ -n "${SKIP_SECRETS_CHECK:-}" ]]; then
echo "skipping secrets check (SKIP_SECRETS_CHECK set)"
elif [[ ! -f "${TOKEN_FILE}" ]]; then
echo "skipping secrets check (no token at ${TOKEN_FILE})"
else
TOKEN="$(cat "${TOKEN_FILE}")"
FORGEJO_API="https://git.breadway.dev/api/v1"
# Full "owner/repo" slugs, deduplicated (bakery + bread-theme both point
# at Breadway/bread-ecosystem, checking it twice is wasted API calls).
mapfile -t registry_slugs < <(python3 -c "
import tomllib
with open('${REGISTRY}', 'rb') as f:
d = tomllib.load(f)
seen = set()
for p in d['products']:
if p['repo'] not in seen:
seen.add(p['repo'])
print(p['repo'])
")
secrets_missing=0
for slug in "${registry_slugs[@]}"; do
has_key="$(curl -s -H "Authorization: token ${TOKEN}" \
"${FORGEJO_API}/repos/${slug}/actions/secrets" \
| python3 -c "
import json, sys
try:
secrets = json.load(sys.stdin)
except json.JSONDecodeError:
secrets = []
print(1 if any(s.get('name') == 'BAKERY_MINISIGN_SEC_KEY_PATH' for s in secrets) else 0)
" 2>/dev/null || echo 0)"
if [[ "${has_key}" != 1 ]]; then
echo "${slug}: missing BAKERY_MINISIGN_SEC_KEY_PATH Actions secret — release CI will fail closed (or worse, publish unsigned on an older workflow shape) until it's set"
secrets_missing=1
fi
done
if [[ "${secrets_missing}" == 0 ]]; then
echo "no missing signing secrets found across ${#registry_slugs[@]} registry repos"
else
drift=1
fi
fi
if [[ "${drift}" == 0 ]]; then
echo "no channel drift found"
else
echo "drift found — see docs/release-channels.md for the policy"
fi
exit "${drift}"