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
305 lines
10 KiB
Bash
Executable File
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))"
|