99e5641127
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
299 lines
12 KiB
Bash
Executable File
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"
|