Skip to content

feat(header): gate consolidated <unraid-header> on Unraid 7.3+ #971

feat(header): gate consolidated <unraid-header> on Unraid 7.3+

feat(header): gate consolidated <unraid-header> on Unraid 7.3+ #971

name: Build PR Plugin
on:
pull_request:
paths:
- 'emhttp/**'
- 'etc/**'
- 'sbin/**'
- 'share/**'
- '.github/workflows/pr-plugin-build.yml'
- '.github/scripts/**'
permissions:
contents: read
pull-requests: read
actions: read
jobs:
build-plugin:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get changed files
id: changed-files
run: |
set -euo pipefail
# Get list of changed files in emhttp, etc, sbin, share using PR SHAs
git diff --name-only "${{ github.event.pull_request.base.sha }}...${{ github.event.pull_request.head.sha }}" | grep -E '^(emhttp|etc|sbin|share)/' > changed_files.txt || true
# Output for debugging
echo "Changed files:"
cat changed_files.txt
# Check if we have any changes
if [ ! -s "changed_files.txt" ]; then
echo "No emhttp files changed"
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Generate plugin version
if: steps.changed-files.outputs.has_changes == 'true'
id: version
run: |
# Generate version with proper timestamp format: YYYY.MM.DD.HHMM
VERSION=$(date -u +"%Y.%m.%d.%H%M")
# Also keep PR info for reference
PR_VERSION="pr.${{ github.event.pull_request.number }}.$(git rev-parse --short HEAD)"
# Generate URLs with versioned TXZ to prevent SHA conflicts
S3_BASE_URL="${{ secrets.CLOUDFLARE_PREVIEW_BUCKET_BASE_URL }}/pr-plugins/pr-${{ github.event.pull_request.number }}"
# Define filenames
LOCAL_TXZ_NAME="webgui-pr-${{ github.event.pull_request.number }}.tar.gz"
REMOTE_TXZ_NAME="webgui-pr-${{ github.event.pull_request.number }}-${VERSION}.tar.gz"
PLUGIN_NAME="webgui-pr-${{ github.event.pull_request.number }}.plg"
# TXZ gets unique versioned path to prevent SHA conflicts on updates
TXZ_URL="${S3_BASE_URL}/${REMOTE_TXZ_NAME}"
PLUGIN_URL="${S3_BASE_URL}/${PLUGIN_NAME}"
TXZ_KEY="pr-plugins/pr-${{ github.event.pull_request.number }}/${REMOTE_TXZ_NAME}"
PLUGIN_KEY="pr-plugins/pr-${{ github.event.pull_request.number }}/${PLUGIN_NAME}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "pr_version=$PR_VERSION" >> $GITHUB_OUTPUT
echo "local_txz=$LOCAL_TXZ_NAME" >> $GITHUB_OUTPUT
echo "remote_txz=$REMOTE_TXZ_NAME" >> $GITHUB_OUTPUT
echo "plugin_name=$PLUGIN_NAME" >> $GITHUB_OUTPUT
echo "txz_url=$TXZ_URL" >> $GITHUB_OUTPUT
echo "plugin_url=$PLUGIN_URL" >> $GITHUB_OUTPUT
echo "txz_key=$TXZ_KEY" >> $GITHUB_OUTPUT
echo "plugin_key=$PLUGIN_KEY" >> $GITHUB_OUTPUT
echo "Generated version: $VERSION (PR: $PR_VERSION)"
- name: Create plugin package
if: steps.changed-files.outputs.has_changes == 'true'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
# Payload layout:
# build/pr.patch unified diff of all changed TEXT files (system-relative paths)
# build/binary/<path> whole copies of changed BINARY files (cannot be diffed)
# build/binary_files.txt list of binary paths (system-relative)
# The plugin applies pr.patch with `patch` so multiple PR plugins can
# stack on the same file; binaries fall back to whole-file replace.
mkdir -p build old new
# Map a repo path to its system-relative install path (no leading slash),
# matching the historical install layout.
map_path() {
case "$1" in
etc/rc.d/*) echo "usr/local/etc/rc.d/${1#etc/rc.d/}" ;;
etc/*) echo "$1" ;;
sbin/*) echo "usr/local/sbin/${1#sbin/}" ;;
share/*) echo "usr/local/share/${1#share/}" ;;
emhttp/*) echo "usr/local/$1" ;;
*) echo "" ;;
esac
}
: > build/binary_files.txt
: > build/text_files.txt
has_text=0
while IFS= read -r file; do
[ -n "$file" ] || continue
sys=$(map_path "$file")
if [ -z "$sys" ]; then echo "Skipping unsupported path: $file"; continue; fi
# numstat reports "-\t-" for binary files (one line per single file)
if git diff --numstat "$BASE_SHA" HEAD -- "$file" | grep -qP '^-\t-\t'; then
if [ -f "$file" ]; then
mkdir -p "build/binary/$(dirname "$sys")"
cp "$file" "build/binary/$sys"
echo "$sys" >> build/binary_files.txt
echo "Binary: $file -> $sys"
else
echo "Skipping deleted binary (unsupported): $file"
fi
continue
fi
# Text: stage base version under old/, head version under new/
if git cat-file -e "$BASE_SHA:$file" 2>/dev/null; then
mkdir -p "old/$(dirname "$sys")"
git show "$BASE_SHA:$file" > "old/$sys"
fi
if [ -f "$file" ]; then
mkdir -p "new/$(dirname "$sys")"
cp "$file" "new/$sys"
fi
has_text=1
echo "$sys" >> build/text_files.txt
echo "Text: $file -> $sys"
done < changed_files.txt
# Unified diff with old/<sys> and new/<sys> headers (apply with patch -p1 at /).
# diff exits 1 when differences exist; only 2 is a real error.
if [ "$has_text" -eq 1 ]; then
diff -ruN old new > build/pr.patch || [ $? -eq 1 ]
echo "pr.patch hunks: $(grep -c '^@@' build/pr.patch || echo 0)"
# Ship the base (original) version of each text file so removal can
# rebuild deterministically: restore originals + re-apply the other
# still-installed PR plugins' patches.
mkdir -p build/orig
cp -a old/. build/orig/ 2>/dev/null || true
fi
# Record the intended mode of every changed file. Unified diffs carry
# no permission bits, so `patch` creates new files with the umask
# (0644) and drops the exec bit. The installer re-applies these modes
# after patching/installing.
: > build/modes.txt
while IFS= read -r file; do
[ -n "$file" ] || continue
[ -f "$file" ] || continue
sys=$(map_path "$file")
[ -n "$sys" ] || continue
printf '%s %s\n' "$(stat -c '%a' "$file")" "$sys" >> build/modes.txt
done < changed_files.txt
echo "modes.txt:"; cat build/modes.txt
find build -type f | sed 's|^build/||' > file_list.txt
echo "Payload contents:"; cat file_list.txt
# Tarball the payload (extracted to a temp dir by the plugin, not to /)
tar -czf "${{ steps.version.outputs.local_txz }}" -C build .
echo "Tarball contents:"
tar -tzf "${{ steps.version.outputs.local_txz }}"
- name: Generate plugin file
if: steps.changed-files.outputs.has_changes == 'true'
run: |
# Generate with placeholder URLs - will be updated by upload workflow
bash .github/scripts/generate-pr-plugin.sh \
"${{ steps.version.outputs.version }}" \
"${{ github.event.pull_request.number }}" \
"$(git rev-parse --short HEAD)" \
"${{ steps.version.outputs.local_txz }}" \
"${{ steps.version.outputs.remote_txz }}" \
"PENDING_UPLOAD" \
"PENDING_UPLOAD"
- name: Save metadata for upload workflow
if: steps.changed-files.outputs.has_changes == 'true'
run: |
cat > pr-metadata.json << EOF
{
"pr_number": ${{ github.event.pull_request.number }},
"version": "${{ steps.version.outputs.version }}",
"pr_version": "${{ steps.version.outputs.pr_version }}",
"local_txz": "${{ steps.version.outputs.local_txz }}",
"remote_txz": "${{ steps.version.outputs.remote_txz }}",
"plugin_name": "${{ steps.version.outputs.plugin_name }}",
"changed_files": $(cat changed_files.txt | jq -R -s -c 'split("\n") | map(select(length > 0))')
}
EOF
echo "Metadata saved:"
cat pr-metadata.json
- name: Upload artifacts to GitHub
if: steps.changed-files.outputs.has_changes == 'true'
uses: actions/upload-artifact@v4
with:
name: webgui-pr-plugin-${{ github.event.pull_request.number }}
path: |
webgui-pr-*.plg
webgui-pr-*.tar.gz
pr-metadata.json
changed_files.txt
retention-days: 30