Files
RedBear-OS/local/scripts/bump-graphics-recipes.sh
T
vasilito 99e5641127 feat: release-bump pipeline + external graphics version sync
Pipeline (3 operator asks):
- bump-release.sh: canonical orchestrator (forks + sources + external)
- upgrade-forks.sh --to=<tag>: rebase forks with diverged-mode guard
- bump-graphics-recipes.sh: map-driven group-aware graphics bumps
- check-external-versions.sh: drift checker for Qt6/KF6/Plasma/Mesa/Wayland
- refresh-fork-upstream-map.sh: append-only map updater with --check
- post-checkout-version-sync.sh + install-git-hooks.sh: opt-in branch hook
- external_version_lib.py: shared version-parsing/bumping library
- external-upstream-map.toml: ~80 external package entries
- bump-fork.sh: deprecated (REDBEAR_I_KNOW_BUMP_FORK_IS_DEPRECATED=1)
- RELEASE-BUMP-WORKFLOW.md: operator runbook

Quality fixes (8 defects from two independent audits):
- blake2b stable cache keys (was hash(), non-portable)
- atomic cache writes via os.replace
- version_sort_key pre-release demotion (was sorting after finals)
- apply_ver_transform re.error tolerance
- grep || true (pipefail abort)
- cd failure detection in upgrade-forks
- sed URL escape (injection hardening)
- refresh-fork-upstream-map last-row drop fix

Doc cleanup:
- Archive 5 obsolete plans to local/docs/archived/
- Remove 14 stale/superseded docs
- Update 18 docs to reference bump-release.sh and fix inbound links
- TOOLS.md drift fixes
2026-07-18 14:45:41 +09:00

299 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# bump-graphics-recipes.sh — Map-driven external source-bump engine.
#
# Rewritten 2026-07-18 from the legacy hardcoded UPSTREAM_INDEX approach.
# The single source of truth is local/external-upstream-map.toml; this script
# never scans directories (symlinked duplicates like recipes/qt/qtbase ↔
# recipes/wip/qt/qtbase are handled by iterating the MAP, which lists each
# canonical local/recipes/ path exactly once).
#
# What it does:
# - Resolves each group's latest version ONCE, then applies to all members.
# - Resolves each singleton's latest version individually.
# - tar bumps: download new tarball → b3sum → sed tar=/blake3= in recipe.toml
# (+ sync [package] version = "..." if present, semver, and differs).
# - git-rev bumps: git ls-remote --tags → latest matching tag → resolve its
# commit sha (annotated tags dereferenced via ^{}) → sed rev = "...".
# - After each non-dry-run bump: runs ./target/release/repo validate-patches
# <recipe> and captures [PASS]/[FAIL] counts.
# - Writes a machine-parseable stabilization report to stdout AND
# .redbear-recipe-bump/last-report.txt.
#
# Usage:
# bump-graphics-recipes.sh [--dry-run] [--no-fetch]
#
# --dry-run resolve and print intended changes; perform no downloads,
# edits, or validate-patches runs.
# --no-fetch reuse cached version indexes under .redbear-recipe-bump/;
# error on entries whose cache is missing.
#
# Never commits. Never deletes recipes. Never creates branches.
# See local/external-upstream-map.toml for the package inventory and schema.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
MAP="$ROOT/local/external-upstream-map.toml"
CACHE_DIR="$ROOT/.redbear-recipe-bump"
REPORT="$CACHE_DIR/last-report.txt"
DRY_RUN=0
NO_FETCH=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--no-fetch) NO_FETCH=1 ;;
--help|-h) grep -E '^#' "$0" | head -30; exit 0 ;;
*) echo "Unknown option: $arg" >&2; exit 1 ;;
esac
done
if [ ! -f "$MAP" ]; then
echo "ERROR: map not found: $MAP" >&2
exit 2
fi
REPO_BIN="$ROOT/target/release/repo"
NO_FETCH_FLAG=""
[ "$NO_FETCH" = "1" ] && NO_FETCH_FLAG="--no-fetch"
mkdir -p "$CACHE_DIR"
# ---------------------------------------------------------------------------
# Phase 1: Python resolves every entry and emits a TSV action plan.
# Fields (tab-separated):
# ACTION RECIPE_NAME RECIPE_DIR KIND OLD NEW NEW_VERSION NEW_PKG_VERSION NOTE
# ACTION ∈ {bump, current, failed}
# ---------------------------------------------------------------------------
ACTIONS_FILE="$(mktemp)"
trap 'rm -f "$ACTIONS_FILE"' EXIT
python3 - "$ROOT" "$NO_FETCH_FLAG" <<'PYEOF' > "$ACTIONS_FILE"
import re
import sys
import tomllib
from pathlib import Path
ROOT = Path(sys.argv[1])
NO_FETCH = "--no-fetch" in sys.argv[2:]
MAP_PATH = ROOT / "local/external-upstream-map.toml"
CACHE_DIR = ROOT / ".redbear-recipe-bump"
with open(MAP_PATH, "rb") as f:
MAP = tomllib.load(f)
sys.path.insert(0, str(ROOT / "local/scripts/lib"))
import external_version_lib
external_version_lib.configure(CACHE_DIR, NO_FETCH)
from external_version_lib import (
fetch, version_sort_key, apply_ver_transform, resolve_index,
render_template, member_url_name, member_template, read_recipe_text,
current_tar_url, current_rev, resolve_git_rev,
)
def current_pkg_version(text):
m = re.search(r'^\[package\]\s*\n(?:[^\[]*?\n)*?\s*version\s*=\s*"([^"]+)"',
text, re.MULTILINE)
return m.group(1) if m else None
def is_semver(v):
return bool(re.match(r'^\d+\.\d+(\.\d+)?$', v or ""))
def emit(action, name, rdir, kind, old, new, new_ver, pkg_ver, note):
print("\t".join([action, name, str(rdir), kind,
old or "", new or "", new_ver or "",
pkg_ver or "", note or ""]))
# Groups
for gid, g in MAP.get("groups", {}).items():
v, _src, err = resolve_index(g["version_index"], g["version_regex"],
g.get("resolve", "version"))
for member in g["members"]:
rdir = ROOT / g["recipe_root"] / member
text = read_recipe_text(rdir)
cur = current_tar_url(text)
tmpl = member_template(g, member)
uname = member_url_name(g, member)
if cur is None:
emit("failed", f"{gid}:{member}", rdir, "tar", "", "", "", "",
"no-tar-in-recipe")
continue
if v is None:
emit("failed", f"{gid}:{member}", rdir, "tar", cur, "", "", "",
f"index:{err}")
continue
new_url = render_template(tmpl, {**v, "name": uname})
if new_url == cur:
emit("current", f"{gid}:{member}", rdir, "tar", cur, new_url,
v["ver"], "", "")
else:
pkg = current_pkg_version(text)
new_pkg = v["ver"] if (pkg and is_semver(pkg) and pkg != v["ver"]) else ""
emit("bump", f"{gid}:{member}", rdir, "tar", cur, new_url,
v["ver"], new_pkg, "")
# Singletons
for pid, p in MAP.get("packages", {}).items():
rdir = ROOT / p["recipe"]
text = read_recipe_text(rdir)
if p["kind"] == "tar":
cur = current_tar_url(text)
if cur is None:
emit("failed", pid, rdir, "tar", "", "", "", "", "no-tar-in-recipe")
continue
v, _src, err = resolve_index(p["version_index"], p["version_regex"],
p.get("resolve", "version"),
p.get("ver_transform"))
if v is None:
emit("failed", pid, rdir, "tar", cur, "", "", "", f"index:{err}")
continue
new_url = render_template(p["tar_template"], {**v, "name": pid})
if new_url == cur:
emit("current", pid, rdir, "tar", cur, new_url, v["ver"], "", "")
else:
pkg = current_pkg_version(text)
new_pkg = v["ver"] if (pkg and is_semver(pkg) and pkg != v["ver"]) else ""
emit("bump", pid, rdir, "tar", cur, new_url, v["ver"], new_pkg, "")
elif p["kind"] == "git-rev":
cur = current_rev(text)
if cur is None:
emit("failed", pid, rdir, "git-rev", "", "", "", "", "no-rev-in-recipe")
continue
res, err = resolve_git_rev(p["git_url"], p["tag_regex"])
if res is None:
emit("failed", pid, rdir, "git-rev", cur, "", "", "", f"git:{err}")
continue
new_sha = res["sha"]
if cur == new_sha or cur.startswith(new_sha[:12]) or cur == res["tag"]:
emit("current", pid, rdir, "git-rev", cur, new_sha, res["tag"], "", "")
else:
emit("bump", pid, rdir, "git-rev", cur, new_sha, res["tag"], "", "")
PYEOF
# ---------------------------------------------------------------------------
# Phase 2: bash applies the action plan.
# ---------------------------------------------------------------------------
UPDATED=0
CURRENT=0
FAILED=0
REPORT_LINES=()
run_validate_patches() {
# $1 = recipe name (basename). Outputs patches_total/pass/fail on stdout
# as "total=N pass=N fail=N". Returns non-zero only if the binary is
# missing (caller treats that as a warning, not a failure).
local recipe="$1"
if [ ! -x "$REPO_BIN" ]; then
echo "total=0 pass=0 fail=0"
echo "WARN: $REPO_BIN not found; skipped validate-patches for $recipe" >&2
return 1
fi
local out
out="$(REDBEAR_CANONICAL_BUILD=1 "$REPO_BIN" validate-patches "$recipe" 2>&1)" || true
local pass fail
pass="$(printf '%s\n' "$out" | grep -c '\[PASS\]' || true)"
fail="$(printf '%s\n' "$out" | grep -c '\[FAIL\]' || true)"
local total=$((pass + fail))
echo "total=${total} pass=${pass} fail=${fail}"
}
# Read the TSV action plan line by line.
while IFS=$'\t' read -r ACTION RNAME RDIR KIND OLD NEW NVER NPKG NOTE; do
[ -n "$ACTION" ] || continue
RTOML="$RDIR/recipe.toml"
case "$ACTION" in
current)
CURRENT=$((CURRENT + 1))
REPORT_LINES+=("recipe=$RNAME old=$OLD new=$NEW patches_total= patches_pass= patches_fail= fail_details=")
if [ "$DRY_RUN" = "1" ]; then
printf '[current] %s (already at %s)\n' "$RNAME" "$NVER"
fi
;;
failed)
FAILED=$((FAILED + 1))
REPORT_LINES+=("recipe=$RNAME old=$OLD new= patches_total= patches_pass= patches_fail= fail_details=$NOTE")
printf '[FAILED] %s: %s\n' "$RNAME" "$NOTE" >&2
;;
bump)
if [ "$DRY_RUN" = "1" ]; then
if [ "$KIND" = "tar" ]; then
printf '[would-bump] %s\n %s\n -> %s\n (would download + b3sum + sed tar=/blake3=)\n' \
"$RNAME" "$OLD" "$NEW"
else
printf '[would-bump] %s\n rev=%s\n -> rev=%s (%s)\n' \
"$RNAME" "$OLD" "$NEW" "$NVER"
fi
UPDATED=$((UPDATED + 1))
REPORT_LINES+=("recipe=$RNAME old=$OLD new=$NEW patches_total= patches_pass= patches_fail= fail_details=(dry-run)")
continue
fi
# --- tar bump ---
if [ "$KIND" = "tar" ]; then
tmp="$(mktemp)"
if ! curl -sLf --retry 3 --retry-delay 2 --connect-timeout 20 --max-time 300 "$NEW" -o "$tmp" 2>/dev/null; then
FAILED=$((FAILED + 1))
REPORT_LINES+=("recipe=$RNAME old=$OLD new=$NEW patches_total= patches_pass= patches_fail= fail_details=download-failed")
printf '[FAILED] %s: download failed for %s\n' "$RNAME" "$NEW" >&2
rm -f "$tmp"
continue
fi
new_hash="$(b3sum "$tmp" | awk '{print $1}')"
rm -f "$tmp"
# Escape sed-special replacement chars (& \ |) so URLs containing
# query strings or other metacharacters do not corrupt the rewrite.
esc_new=$(printf '%s' "$NEW" | sed 's/[&\\|]/\\&/g')
sed -i -E "s|^\\s*tar\\s*=\\s*\"[^\"]+\"|tar = \"$esc_new\"|" "$RTOML"
sed -i -E "s|^\\s*blake3\\s*=\\s*\"[a-f0-9]+\"|blake3 = \"$new_hash\"|" "$RTOML"
if [ -n "$NPKG" ]; then
sed -i -E "s|^(\\s*version\\s*=\\s*)\"[^\"]+\"|\\1\"$NPKG\"|" "$RTOML"
fi
UPDATED=$((UPDATED + 1))
# --- git-rev bump ---
elif [ "$KIND" = "git-rev" ]; then
sed -i -E "s|^\\s*rev\\s*=\\s*\"[^\"]+\"|rev = \"$NEW\"|" "$RTOML"
UPDATED=$((UPDATED + 1))
fi
# --- validate-patches ---
validate_out="$(run_validate_patches "$(basename "$RDIR")" 2>/dev/null || true)"
vt="$(printf '%s' "$validate_out" | grep -oE 'total=[0-9]*' | head -1 | cut -d= -f2)"
vp="$(printf '%s' "$validate_out" | grep -oE 'pass=[0-9]*' | head -1 | cut -d= -f2)"
vf="$(printf '%s' "$validate_out" | grep -oE 'fail=[0-9]*' | head -1 | cut -d= -f2)"
vt="${vt:-0}"; vp="${vp:-0}"; vf="${vf:-0}"
REPORT_LINES+=("recipe=$RNAME old=$OLD new=$NEW patches_total=$vt patches_pass=$vp patches_fail=$vf fail_details=")
;;
esac
done < "$ACTIONS_FILE"
# ---------------------------------------------------------------------------
# Phase 3: report + summary.
# ---------------------------------------------------------------------------
{
for line in "${REPORT_LINES[@]}"; do
printf '%s\n' "$line"
done
} | tee "$REPORT"
echo ""
echo "=== Bump summary ==="
echo "Updated (or would-update): $UPDATED"
echo "Already current: $CURRENT"
echo "Failed (network/parse): $FAILED"
[ "$DRY_RUN" = "1" ] && echo "(dry-run; no files changed)"
echo "Report written to: $REPORT"