Files
RedBear-OS/local/scripts/refresh-fork-upstream-map.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

305 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
# refresh-fork-upstream-map.sh — Update column 3 (the upstream release tag)
# of local/fork-upstream-map.toml in place, preserving every other byte.
#
# Policy (release-bump-pipeline.md, Deliverable C):
# - APPEND-ONLY over column 3 (the upstream release tag).
# - Preserve columns 1 (fork), 2 (url), 4 (mode) and ALL comments / header
# text byte-for-byte. The curated header comments and the `base` row are
# never dropped.
# - SKIP ls-remote for rows with mode `diverged` — their rebase is operator
# manual (Phase 2.4+); the map tag documents the CURRENT base, not the
# latest upstream.
# - SKIP ls-remote for rows whose tag is `main` (the `base` row, which has
# no upstream releases).
# - For forks present in local/sources/*/ with an upstream remote but
# MISSING from the map: APPEND a new row
# `<fork> <url> <latest-tag> # TODO: classify mode`
# at the end of the file.
#
# Usage:
# ./local/scripts/refresh-fork-upstream-map.sh # Update col 3 in place
# ./local/scripts/refresh-fork-upstream-map.sh --check # Read-only report, exit 1 on drift
# ./local/scripts/refresh-fork-upstream-map.sh --no-fetch # Use map's current tags (no network)
# ./local/scripts/refresh-fork-upstream-map.sh --help # This header
#
# Acceptance:
# - `git diff local/fork-upstream-map.toml` shows only column-3 token changes
# (if any) plus any newly appended rows.
# - `grep -c '^base\b' local/fork-upstream-map.toml` = 1.
# - `grep -c diverged local/fork-upstream-map.toml` unchanged vs before.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
MAP="$ROOT/local/fork-upstream-map.toml"
CHECK_ONLY=0
NO_FETCH=0
for arg in "$@"; do
case "$arg" in
--check) CHECK_ONLY=1 ;;
--no-fetch) NO_FETCH=1 ;;
-h|--help) sed -n '2,35p' "$0"; exit 0 ;;
*) echo "Unknown option: $arg" >&2; exit 1 ;;
esac
done
if [[ ! -f "$MAP" ]]; then
echo "ERROR: map file not found: $MAP" >&2
exit 1
fi
# Anchored semver regex (release-bump-pipeline.md design constraint #2).
SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+$'
# ---- latest_tag_for_url <url> [fork] [base] --------------------------------
# Resolve the newest GENUINE upgrade tag for a remote via `git ls-remote`.
# Output: "<tag>|<raw-count>" — tag empty when nothing qualifies.
# Two filters: (a) tags already reachable from the fork's HEAD are history,
# not newer releases (relibc's 0.6.0/2020 outranks its newer 0.2.x API line
# lexically); (b) tags not version-greater than the current base are not
# newer either (the base tag itself is usually an ancestor of HEAD).
latest_tag_for_url() {
local url="$1" fork="${2:-}" base="${3:-}"
local tag forkdir="" raw=0
[[ -n "$fork" && -d "$ROOT/local/sources/$fork/.git" ]] \
&& forkdir="$ROOT/local/sources/$fork"
while IFS= read -r tag; do
[[ -z "$tag" ]] && continue
raw=$((raw + 1))
if [[ -n "$forkdir" ]] \
&& git -C "$forkdir" rev-parse --verify --quiet "refs/tags/$tag" >/dev/null 2>&1 \
&& git -C "$forkdir" merge-base --is-ancestor "refs/tags/$tag" HEAD 2>/dev/null; then
continue
fi
if [[ -n "$base" ]] && ! version_greater "$tag" "$base"; then
continue
fi
echo "${tag}|${raw}"
return 0
done < <(git ls-remote --tags --sort=-v:refname "$url" 2>/dev/null \
| awk '{print $2}' \
| sed 's|refs/tags/||' \
| sed 's|\^{}$||' \
| grep -E "$SEMVER_RE" || true)
echo "|${raw}"
return 0
}
# version_greater <a> <b> → true iff a is version-greater than b (sort -V).
version_greater() {
[[ "$1" != "$2" ]] \
&& [[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | tail -1)" == "$1" ]]
}
# ---- parse existing map into associative arrays ----------------------------
# Sets MAP_FORK_URL[fork]=url, MAP_FORK_TAG[fork]=tag, MAP_FORK_MODE[fork]=mode.
declare -A MAP_FORK_URL=()
declare -A MAP_FORK_TAG=()
declare -A MAP_FORK_MODE=()
# Match a data row: first non-whitespace, non-# token = fork; followed by
# whitespace-separated url, tag, optional mode + trailing comment.
# We use awk to emit TAB-separated columns; bash reads them in.
while IFS=$'\t' read -r fork url tag mode; do
[[ -z "$fork" ]] && continue
MAP_FORK_URL["$fork"]="$url"
MAP_FORK_TAG["$fork"]="$tag"
MAP_FORK_MODE["$fork"]="$mode"
done < <(awk '
/^[^#[:space:]]/ {
fork = $1
url = $2
tag = $3
mode = $4
sub(/[ \t]*#.*/, "", mode)
print fork "\t" url "\t" tag "\t" mode
}
' "$MAP")
# ---- collect on-disk forks with upstream remote ----------------------------
declare -A DISK_FORK_URL=()
for d in "$ROOT"/local/sources/*/; do
[[ -d "$d/.git" ]] || continue
name=$(basename "$d")
url=$(git -C "$d" remote get-url upstream 2>/dev/null || true)
[[ -n "$url" ]] && DISK_FORK_URL["$name"]="$url"
done
# ---- plan column-3 updates + appends ---------------------------------------
declare -A NEW_TAG_FOR=()
declare -a APPEND_ROWS=()
DRIFT_COUNT=0
UPDATE_COUNT=0
echo "=== refresh-fork-upstream-map ==="
if [[ "$NO_FETCH" -eq 1 ]]; then
echo "(--no-fetch: skipping git ls-remote; using current map tags)"
fi
echo ""
# Iterate in map-order (preserve stable output for --check reports).
MAP_ORDER=()
while IFS= read -r line; do
if [[ "$line" =~ ^[^#[:space:]] ]]; then
fork=$(awk '{print $1}' <<<"$line")
MAP_ORDER+=("$fork")
fi
done < <(awk 1 "$MAP")
for fork in "${MAP_ORDER[@]}"; do
url="${MAP_FORK_URL[$fork]:-}"
current="${MAP_FORK_TAG[$fork]:-}"
mode="${MAP_FORK_MODE[$fork]:-}"
# Skip the base row (tag=main).
if [[ "$current" == "main" ]]; then
echo "fork=$fork mode=$mode current=$current latest=(skip) action=skip-main"
continue
fi
# Skip diverged forks: tag documents CURRENT base.
if [[ "$mode" == "diverged" ]]; then
echo "fork=$fork mode=diverged current=$current latest=(skip) action=skip-diverged"
continue
fi
latest=""
if [[ "$NO_FETCH" -eq 0 ]]; then
result=$(latest_tag_for_url "$url" "$fork" "$current")
latest="${result%%|*}"
raw="${result##*|}"
# Tags existed upstream but none qualified as newer → already current.
[[ -z "$latest" && "$raw" -gt 0 ]] && latest="$current"
fi
if [[ -z "$latest" ]]; then
echo "fork=$fork mode=$mode current=$current latest=(unknown) action=skip-no-tag"
continue
fi
if [[ "$latest" != "$current" ]]; then
NEW_TAG_FOR["$fork"]="$latest"
if [[ $CHECK_ONLY -eq 1 ]]; then
echo "fork=$fork mode=$mode current=$current latest=$latest action=DRIFT"
DRIFT_COUNT=$((DRIFT_COUNT + 1))
else
echo "fork=$fork mode=$mode current=$current latest=$latest action=UPDATE"
UPDATE_COUNT=$((UPDATE_COUNT + 1))
fi
else
echo "fork=$fork mode=$mode current=$current latest=$latest action=ok"
fi
done
# Forks on disk but missing from the map → append.
# Iterate in sorted order for stable output.
for fork in $(printf '%s\n' "${!DISK_FORK_URL[@]}" | sort); do
if [[ -z "${MAP_FORK_URL[$fork]:-}" ]]; then
url="${DISK_FORK_URL[$fork]}"
latest=""
if [[ "$NO_FETCH" -eq 0 ]]; then
result=$(latest_tag_for_url "$url" "$fork")
latest="${result%%|*}"
fi
latest="${latest:-unknown}"
row="$fork $url $latest # TODO: classify mode"
if [[ $CHECK_ONLY -eq 1 ]]; then
echo "fork=$fork mode=(missing) current=(none) latest=$latest action=DRIFT-MISSING"
DRIFT_COUNT=$((DRIFT_COUNT + 1))
else
echo "fork=$fork mode=(missing) current=(none) latest=$latest action=APPEND"
APPEND_ROWS+=("$row")
fi
fi
done
# ---- --check exit ----------------------------------------------------------
if [[ $CHECK_ONLY -eq 1 ]]; then
echo ""
echo "Summary: drift=$DRIFT_COUNT"
if [[ $DRIFT_COUNT -gt 0 ]]; then
echo "FAIL: $DRIFT_COUNT drift(s) detected" >&2
exit 1
fi
echo "OK: no drift"
exit 0
fi
# ---- apply column-3 rewrites in place (byte-exact awk) ---------------------
if [[ ${#NEW_TAG_FOR[@]} -eq 0 && ${#APPEND_ROWS[@]} -eq 0 ]]; then
echo ""
echo "No changes needed ($MAP already current)"
exit 0
fi
TMP="${MAP}.tmp.$$"
APPEND_FILE=""
trap 'rm -f "$TMP" "$APPEND_FILE"' EXIT
# Sidecar file with append rows (avoids awk quoting issues).
if [[ ${#APPEND_ROWS[@]} -gt 0 ]]; then
APPEND_FILE="${TMP}.append"
: > "$APPEND_FILE"
for row in "${APPEND_ROWS[@]}"; do
printf '%s\n' "$row" >> "$APPEND_FILE"
done
fi
# Two-pass awk:
# pass 1 (control): UPDATES[fork] = newtag
# pass 2 (map): rewrite column 3 in matching rows; everything else
# is printed verbatim. Append sidecar rows at END.
#
# The column-3 rewrite uses match() to locate the prefix
# `^fork<ws>url<ws>` and the next non-whitespace run (the tag), so all
# leading whitespace, the URL, and everything after the tag (mode,
# comments, EOL) are preserved byte-for-byte.
#
# A sentinel record guarantees the control stream always has at least one
# line. Without it, when CONTROL is empty (only appends, no col-3
# rewrites), awk's `NR == FNR` test would misidentify every map line as a
# control line and corrupt the output.
CONTROL=$'#__rbum_sentinel__\t0.0.0\n'
for fork in "${!NEW_TAG_FOR[@]}"; do
CONTROL+="${fork}"$'\t'"${NEW_TAG_FOR[$fork]}"$'\n'
done
awk -v APPEND_FILE="$APPEND_FILE" '
NR == FNR {
UPDATES[$1] = $2
next
}
{
if ($0 ~ /^[^#[:space:]]/) {
fork = $1
if (fork in UPDATES) {
prefix_re = fork "[ \t]+[^ \t]+[ \t]+"
if (match($0, prefix_re)) {
prefix = substr($0, 1, RLENGTH)
rest = substr($0, RLENGTH + 1)
if (match(rest, /^[^ \t]+/)) {
suffix = substr(rest, RLENGTH + 1)
$0 = prefix UPDATES[fork] suffix
}
}
}
}
print
}
END {
if (APPEND_FILE != "") {
while ((getline line < APPEND_FILE) > 0) print line
close(APPEND_FILE)
}
}
' <(printf '%s' "$CONTROL") "$MAP" > "$TMP"
mv "$TMP" "$MAP"
echo ""
echo "Updated $MAP ($UPDATE_COUNT col-3 rewrite(s), ${#APPEND_ROWS[@]} append(s))"