Skip to content

fix(ci): preserve RHDH-specific template patches across upstream syncs#406

Merged
rm3l merged 2 commits into
redhat-developer:mainfrom
rm3l:fix/preserve-rhdh-patches-on-upstream-sync
May 18, 2026
Merged

fix(ci): preserve RHDH-specific template patches across upstream syncs#406
rm3l merged 2 commits into
redhat-developer:mainfrom
rm3l:fix/preserve-rhdh-patches-on-upstream-sync

Conversation

@rm3l
Copy link
Copy Markdown
Member

@rm3l rm3l commented May 18, 2026

Description of the change

The weekly sync-upstream-backstage.yaml workflow uses git subtree pull to sync the vendored Backstage chart from upstream. This silently overwrites RHDH-specific modifications to vendored templates (e.g., Lightspeed integration, catalog index images, checksum annotations in backstage-deployment.yaml), even when there are no merge conflicts; Git's merge algorithm auto-resolves in favor of upstream.

This PR adds a sync script (hack/sync-upstream-backstage.sh) that wraps the full sync flow:

  1. Generates a patch of RHDH-specific template modifications before the subtree pull
  2. Performs the subtree pull (which resets vendored files to upstream)
  3. Re-applies the RHDH patch on top of the updated upstream
  4. Applies .gitignore fixups and rebuilds vendored .tgz dependencies
  5. Commits the result

If the patch fails to apply (because upstream changed the same lines), the script saves the patch to rhdh-vendored.patch and exits with an error, so conflicts are surfaced explicitly rather than silently dropped.

The CI workflow now calls this script, and maintainers can use it locally for manual syncs as well.

Which issue(s) does this PR fix or relate to

How to test changes / Special notes to the reviewer

  1. Round-trip verification (already tested locally):

    • Generate the RHDH patch from the current state
    • Reset the vendored backstage-deployment.yaml to the upstream version
    • Apply the patch
    • Confirm the result is byte-identical to the current RHDH-modified version
  2. Script test: Run ./hack/sync-upstream-backstage.sh locally. Since we're already in sync with upstream, it should report "No changes from upstream" and exit cleanly.

  3. Conflict handling: To test the failure path, manually edit a patched region of the upstream file before applying — the script should fail with a helpful message and save rhdh-vendored.patch.

Checklist

  • For each Chart updated, version bumped in the corresponding Chart.yaml according to Semantic Versioning.
    • N/A — no chart changes, only CI/workflow/docs
  • For each Chart updated, variables are documented in the values.yaml and added to the corresponding README.md. The pre-commit utility can be used to generate the necessary content. Run pre-commit run --all-files to run the hooks and then push any resulting changes. The pre-commit Workflow will enforce this and warn you if needed.
    • N/A — no chart variable changes
  • JSON Schema template updated and re-generated the raw schema via the pre-commit hook.
    • N/A — no schema changes
  • Tests pass using the Chart Testing tool and the ct lint command.
    • N/A — no chart template changes
  • If you updated the orchestrator-infra chart, make sure the versions of the Knative CRDs are aligned with the versions of the CRDs installed by the OpenShift Serverless operators declared in the values.yaml file. See Installing Knative Eventing and Knative Serving CRDs for more details.
    • N/A — orchestrator-infra not touched

The weekly sync workflow's subtree pull silently overwrites RHDH-specific
modifications to vendored templates (lightspeed, catalog index images,
checksums). Add a sync script that generates a patch of RHDH changes
before the pull and re-applies it after, failing loudly if conflicts
arise instead of silently dropping changes.
@rm3l rm3l requested a review from a team as a code owner May 18, 2026 08:34
@rhdh-qodo-merge
Copy link
Copy Markdown

rhdh-qodo-merge Bot commented May 18, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Patch uses wrong base 🐞 Bug ≡ Correctness
Description
hack/sync-upstream-backstage.sh generates the “RHDH patch” by diffing current vendored templates
against the target upstream ref (REMOTE/REF), so when the repo is behind upstream the patch will
include upstream deltas and then revert them after the subtree pull. This can yield sync PRs that
silently drop upstream template updates instead of bringing them in.
Code

hack/sync-upstream-backstage.sh[R56-139]

+echo "Fetching ${REMOTE}/${REF}..."
+git fetch "$REMOTE" "$REF"
+
+# ── Generate RHDH-specific patch ─────────────────────────────────────
+PATCH_FILE=$(mktemp "${TMPDIR:-/tmp}/rhdh-patch.XXXXXX")
+cleanup() { rm -f "$PATCH_FILE"; }
+trap cleanup EXIT
+
+echo "Generating RHDH-specific template patches..."
+
+has_meaningful_diff() {
+  # A diff is meaningful if added and removed lines differ in content,
+  # not just in trailing whitespace or newline presence.
+  local added removed
+  added=$(sed -n 's/^+//p' "$1" | grep -v '^++' | sed 's/[[:space:]]*$//' | sort)
+  removed=$(sed -n 's/^-//p' "$1" | grep -v '^--' | sed 's/[[:space:]]*$//' | sort)
+  [ "$added" != "$removed" ]
+}
+
+for vendored_file in "${VENDOR_TEMPLATES}"/*.yaml; do
+  [ -f "$vendored_file" ] || continue
+  filename=$(basename "$vendored_file")
+
+  # Only diff files that also exist upstream; RHDH-only files won't be
+  # touched by the subtree pull so they don't need patching.
+  upstream_content=$(git show "${REMOTE}/${REF}:${UPSTREAM_TEMPLATES}/${filename}" 2>/dev/null) || continue
+
+  # Produce a unified diff with paths relative to the repo root so
+  # git-apply works from the top level.
+  FILE_DIFF=$(mktemp "${TMPDIR:-/tmp}/rhdh-filediff.XXXXXX")
+  diff -u <(printf '%s\n' "$upstream_content") "$vendored_file" \
+    | sed "1s|^--- .*|--- a/${VENDOR_TEMPLATES}/${filename}|
+           2s|^+++ .*|+++ b/${VENDOR_TEMPLATES}/${filename}|" \
+    > "$FILE_DIFF" || true   # diff exits 1 when files differ
+
+  if [ -s "$FILE_DIFF" ] && has_meaningful_diff "$FILE_DIFF"; then
+    cat "$FILE_DIFF" >> "$PATCH_FILE"
+  fi
+  rm -f "$FILE_DIFF"
+done
+
+if [ -s "$PATCH_FILE" ]; then
+  patched_files=$(grep -c '^--- a/' "$PATCH_FILE" || true)
+  echo "  Found patches for ${patched_files} file(s)."
+else
+  echo "  No RHDH-specific template patches to preserve."
+fi
+
+# ── Subtree pull ─────────────────────────────────────────────────────
+BEFORE_SHA=$(git rev-parse HEAD)
+
+echo "Pulling upstream subtree..."
+git subtree pull --prefix "$PREFIX" "$REMOTE" "$REF" --squash \
+  -m "Squashed sync of upstream Backstage chart"
+
+AFTER_SHA=$(git rev-parse HEAD)
+
+if [ "$BEFORE_SHA" = "$AFTER_SHA" ]; then
+  echo "No changes from upstream."
+  exit 0
+fi
+
+echo "Upstream changes merged."
+
+# ── Re-apply RHDH patches ───────────────────────────────────────────
+if [ -s "$PATCH_FILE" ]; then
+  echo "Re-applying RHDH-specific template patches..."
+  if ! git apply "$PATCH_FILE"; then
+    cp "$PATCH_FILE" rhdh-vendored.patch
+    trap - EXIT
+    echo ""
+    echo "ERROR: RHDH patch failed to apply cleanly."
+    echo "The patch has been saved to: rhdh-vendored.patch"
+    echo ""
+    echo "To resolve:"
+    echo "  1. Review the patch:  cat rhdh-vendored.patch"
+    echo "  2. Try with 3-way:    git apply --3way rhdh-vendored.patch"
+    echo "  3. Or with rejects:   git apply --reject rhdh-vendored.patch"
+    echo "  4. Resolve any .rej files, then:  git add <files>"
+    echo "  5. Clean up:          rm rhdh-vendored.patch"
+    exit 1
+  fi
+  echo "  RHDH template patches re-applied successfully."
+fi
Relevance

⭐⭐ Medium

No historical evidence for subtree-sync patch baseline correctness; related subtree workflow exists
but no similar reviews.

PR-#325

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The patch baseline is read from ${REMOTE}/${REF} (target upstream) and then applied after pulling
that same ref, which will necessarily revert any upstream changes included in that diff when the
repo is behind upstream.

hack/sync-upstream-backstage.sh[56-89]
hack/sync-upstream-backstage.sh[104-139]
.github/workflows/sync-upstream-backstage.yaml[42-58]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The script builds a patch by comparing vendored templates to `${REMOTE}/${REF}` (the *new* upstream HEAD), then after `git subtree pull` (which updates files to `${REMOTE}/${REF}`) it `git apply`s that patch. When the vendored subtree is behind upstream, this patch necessarily contains upstream changes (not just RHDH customizations) and will undo upstream template updates.

### Issue Context
We need the patch to represent *only* RHDH-local modifications on top of the currently-vendored upstream version, so it can be reapplied after updating the subtree.

### Fix Focus Areas
- hack/sync-upstream-backstage.sh[47-139]

### Suggested fix approach
1. Determine the upstream commit that corresponds to the currently-vendored subtree (the previous upstream base), not the target `${REMOTE}/${REF}`.
  - With `git subtree pull --squash`, the squashed sync commits typically contain a `git-subtree-split: <sha>` trailer; locate the most recent commit for this `--prefix` and extract that split SHA.
2. Use that extracted SHA as the baseline for `git show` when computing `upstream_content`, e.g. `git show "${SPLIT_SHA}:${UPSTREAM_TEMPLATES}/${filename}"`.
3. Only after generating the patch against the *current* upstream base, run `git subtree pull ... ${REMOTE} ${REF}` and re-apply the patch.
4. If no prior subtree split SHA can be found, fail fast with a clear error (so the workflow doesn’t create a misleading PR), or fall back to a safer mechanism (e.g., compute the patch as the diff between the last subtree-sync commit and HEAD for the vendored templates).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Unconditional git add fails 🐞 Bug ☼ Reliability
Description
The script only edits the vendored .gitignore if it exists, but later unconditionally runs `git
add "$VENDOR_GITIGNORE" under set -e`, which will terminate the sync if upstream removes/renames
that file. This makes the weekly CI sync brittle to upstream layout changes.
Code

hack/sync-upstream-backstage.sh[R25-166]

+set -euo pipefail
+
+REMOTE="upstream-backstage"
+REF="main"
+PREFIX="charts/backstage/vendor/backstage"
+UPSTREAM_URL="https://github.com/backstage/charts.git"
+
+usage() {
+  sed -n '2,/^$/s/^# \{0,1\}//p' "$0"
+  exit "${1:-0}"
+}
+
+while [ $# -gt 0 ]; do
+  case "$1" in
+    --remote) REMOTE="$2"; shift 2 ;;
+    --ref)    REF="$2";    shift 2 ;;
+    --prefix) PREFIX="$2"; shift 2 ;;
+    -h|--help) usage 0 ;;
+    *) echo "Unknown option: $1" >&2; usage 1 ;;
+  esac
+done
+
+UPSTREAM_TEMPLATES="charts/backstage/templates"
+VENDOR_TEMPLATES="${PREFIX}/charts/backstage/templates"
+VENDOR_GITIGNORE="${PREFIX}/.gitignore"
+
+# ── Ensure upstream remote exists and is fetched ─────────────────────
+if ! git remote get-url "$REMOTE" &>/dev/null; then
+  echo "Adding remote ${REMOTE} -> ${UPSTREAM_URL}"
+  git remote add "$REMOTE" "$UPSTREAM_URL"
+fi
+echo "Fetching ${REMOTE}/${REF}..."
+git fetch "$REMOTE" "$REF"
+
+# ── Generate RHDH-specific patch ─────────────────────────────────────
+PATCH_FILE=$(mktemp "${TMPDIR:-/tmp}/rhdh-patch.XXXXXX")
+cleanup() { rm -f "$PATCH_FILE"; }
+trap cleanup EXIT
+
+echo "Generating RHDH-specific template patches..."
+
+has_meaningful_diff() {
+  # A diff is meaningful if added and removed lines differ in content,
+  # not just in trailing whitespace or newline presence.
+  local added removed
+  added=$(sed -n 's/^+//p' "$1" | grep -v '^++' | sed 's/[[:space:]]*$//' | sort)
+  removed=$(sed -n 's/^-//p' "$1" | grep -v '^--' | sed 's/[[:space:]]*$//' | sort)
+  [ "$added" != "$removed" ]
+}
+
+for vendored_file in "${VENDOR_TEMPLATES}"/*.yaml; do
+  [ -f "$vendored_file" ] || continue
+  filename=$(basename "$vendored_file")
+
+  # Only diff files that also exist upstream; RHDH-only files won't be
+  # touched by the subtree pull so they don't need patching.
+  upstream_content=$(git show "${REMOTE}/${REF}:${UPSTREAM_TEMPLATES}/${filename}" 2>/dev/null) || continue
+
+  # Produce a unified diff with paths relative to the repo root so
+  # git-apply works from the top level.
+  FILE_DIFF=$(mktemp "${TMPDIR:-/tmp}/rhdh-filediff.XXXXXX")
+  diff -u <(printf '%s\n' "$upstream_content") "$vendored_file" \
+    | sed "1s|^--- .*|--- a/${VENDOR_TEMPLATES}/${filename}|
+           2s|^+++ .*|+++ b/${VENDOR_TEMPLATES}/${filename}|" \
+    > "$FILE_DIFF" || true   # diff exits 1 when files differ
+
+  if [ -s "$FILE_DIFF" ] && has_meaningful_diff "$FILE_DIFF"; then
+    cat "$FILE_DIFF" >> "$PATCH_FILE"
+  fi
+  rm -f "$FILE_DIFF"
+done
+
+if [ -s "$PATCH_FILE" ]; then
+  patched_files=$(grep -c '^--- a/' "$PATCH_FILE" || true)
+  echo "  Found patches for ${patched_files} file(s)."
+else
+  echo "  No RHDH-specific template patches to preserve."
+fi
+
+# ── Subtree pull ─────────────────────────────────────────────────────
+BEFORE_SHA=$(git rev-parse HEAD)
+
+echo "Pulling upstream subtree..."
+git subtree pull --prefix "$PREFIX" "$REMOTE" "$REF" --squash \
+  -m "Squashed sync of upstream Backstage chart"
+
+AFTER_SHA=$(git rev-parse HEAD)
+
+if [ "$BEFORE_SHA" = "$AFTER_SHA" ]; then
+  echo "No changes from upstream."
+  exit 0
+fi
+
+echo "Upstream changes merged."
+
+# ── Re-apply RHDH patches ───────────────────────────────────────────
+if [ -s "$PATCH_FILE" ]; then
+  echo "Re-applying RHDH-specific template patches..."
+  if ! git apply "$PATCH_FILE"; then
+    cp "$PATCH_FILE" rhdh-vendored.patch
+    trap - EXIT
+    echo ""
+    echo "ERROR: RHDH patch failed to apply cleanly."
+    echo "The patch has been saved to: rhdh-vendored.patch"
+    echo ""
+    echo "To resolve:"
+    echo "  1. Review the patch:  cat rhdh-vendored.patch"
+    echo "  2. Try with 3-way:    git apply --3way rhdh-vendored.patch"
+    echo "  3. Or with rejects:   git apply --reject rhdh-vendored.patch"
+    echo "  4. Resolve any .rej files, then:  git add <files>"
+    echo "  5. Clean up:          rm rhdh-vendored.patch"
+    exit 1
+  fi
+  echo "  RHDH template patches re-applied successfully."
+fi
+
+# ── Apply .gitignore and .tgz fixups ────────────────────────────────
+RHDH_MARKER="# RHDH: track vendored chart dependencies"
+
+# Fix directory ignore pattern so negation rules work
+if [ -f "$VENDOR_GITIGNORE" ]; then
+  sed -i'' -e 's|^charts/\*/charts/$|charts/*/charts/*|' "$VENDOR_GITIGNORE"
+
+  if ! grep -q "$RHDH_MARKER" "$VENDOR_GITIGNORE"; then
+    cat >> "$VENDOR_GITIGNORE" <<EOF
+
+${RHDH_MARKER}
+# Since this chart is vendored, we commit its dependencies rather than fetching them at install time
+!charts/*/charts/*.tgz
+EOF
+  fi
+fi
+
+# Rebuild vendored chart dependencies to restore .tgz files
+helm dependency update "${PREFIX}/charts/backstage"
+
+# ── Commit RHDH-specific changes ────────────────────────────────────
+git add "$VENDOR_GITIGNORE"
+git add "${PREFIX}/charts/backstage/charts/"*.tgz 2>/dev/null || true
+git add "${VENDOR_TEMPLATES}/"
+if ! git diff --cached --quiet; then
+  git commit -m "chore: apply RHDH-specific changes to vendored Backstage chart"
Relevance

⭐⭐ Medium

No similar past finding; repo does accept CI hardening to avoid brittle failures (e.g., PR 306).

PR-#306

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The script is running with set -euo pipefail, checks for the file before editing, but still stages
it unconditionally afterward, which will error if the file doesn’t exist.

hack/sync-upstream-backstage.sh[25-26]
hack/sync-upstream-backstage.sh[145-156]
hack/sync-upstream-backstage.sh[161-166]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`git add "$VENDOR_GITIGNORE"` is executed unconditionally while the file itself is only conditionally present/edited. Under `set -e`, a missing file will cause the script (and CI sync workflow) to fail.

### Issue Context
The script already guards the sed/append operations with `[ -f "$VENDOR_GITIGNORE" ]`, but not the `git add`.

### Fix Focus Areas
- hack/sync-upstream-backstage.sh[141-166]

### Suggested fix
Wrap the `git add "$VENDOR_GITIGNORE"` in the same existence check, or make it non-fatal, e.g.:
- `if [ -f "$VENDOR_GITIGNORE" ]; then git add "$VENDOR_GITIGNORE"; fi`
 
(Prefer the explicit existence check to avoid hiding other `git add` errors.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@rhdh-qodo-merge
Copy link
Copy Markdown

Review Summary by Qodo

Preserve RHDH template patches across upstream Backstage syncs

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Add sync script to preserve RHDH template patches during upstream syncs
• Prevent silent loss of RHDH-specific modifications to vendored Backstage chart
• Surface patch conflicts explicitly with saved patch file for manual resolution
• Simplify CI workflow by delegating sync logic to reusable script
• Update developer documentation with conflict resolution guidance
Diagram
flowchart LR
  A["Upstream Backstage"] -->|fetch| B["Generate RHDH Patch"]
  B -->|save patch| C["Git Subtree Pull"]
  C -->|reset to upstream| D["Re-apply RHDH Patch"]
  D -->|success| E["Apply Fixups & Commit"]
  D -->|conflict| F["Save rhdh-vendored.patch"]
  E --> G["Sync Complete"]
  F --> H["Manual Resolution Required"]
Loading

Grey Divider

File Changes

1. hack/sync-upstream-backstage.sh ✨ Enhancement +169/-0

New sync script preserving RHDH template patches

• New POSIX-compatible script that wraps the full upstream sync workflow
• Generates patch of RHDH-specific template modifications before subtree pull
• Re-applies RHDH patch after upstream sync and handles merge conflicts
• Applies .gitignore fixups and rebuilds vendored .tgz dependencies
• Supports --remote, --ref, and --prefix command-line options

hack/sync-upstream-backstage.sh


2. .github/workflows/sync-upstream-backstage.yaml ✨ Enhancement +3/-37

Simplify CI workflow to use sync script

• Simplified sync step to call hack/sync-upstream-backstage.sh instead of inline git commands
• Removed manual .gitignore and .tgz fixup steps (now handled by script)
• Updated PR body text to reflect automatic patch re-application instead of caution warning
• Reduced workflow complexity by delegating sync logic to reusable script

.github/workflows/sync-upstream-backstage.yaml


3. CONTRIBUTING.md 📝 Documentation +20/-11

Document sync script and conflict resolution workflow

• Updated developer workflow to use hack/sync-upstream-backstage.sh script
• Documented automatic patch generation and re-application process
• Added conflict resolution guidance with step-by-step instructions
• Replaced caution about silent overwrites with note about explicit conflict handling
• Added reference to weekly CI workflow using the same script

CONTRIBUTING.md


Grey Divider

Qodo Logo

@rhdh-qodo-merge rhdh-qodo-merge Bot added documentation Improvements or additions to documentation enhancement New feature or request bug_fix labels May 18, 2026
- Use [[ ]] instead of [ ] for conditional tests (S7688)
- Assign positional parameter to local variable in has_meaningful_diff (S7679)
- Redirect error messages to stderr (S7677)
@sonarqubecloud
Copy link
Copy Markdown

@rm3l rm3l merged commit 2747d18 into redhat-developer:main May 18, 2026
7 of 8 checks passed
@rm3l rm3l deleted the fix/preserve-rhdh-patches-on-upstream-sync branch May 18, 2026 09:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug_fix documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant