Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/pr-preview-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Build CA PR preview

on:
pull_request:
types: [opened, synchronize, reopened]

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build installable preview
run: ./scripts/build-pr-preview.sh "${{ github.event.pull_request.number }}" "${{ github.event.pull_request.head.sha }}"
- uses: actions/upload-artifact@v4
with:
name: ca-pr-preview
path: dist/pr-preview/
if-no-files-found: error
retention-days: 30
30 changes: 30 additions & 0 deletions .github/workflows/pr-preview-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Remove CA PR preview

on:
pull_request_target:
types: [closed]

permissions:
contents: write

concurrency:
group: ca-pr-preview-publish

jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: pr-previews
- name: Remove closed PR artifacts
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
test -d "pr/$PR_NUMBER" || exit 0
rm -rf "pr/$PR_NUMBER"
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git add -A "pr/$PR_NUMBER"
git commit -m "preview: remove closed PR #$PR_NUMBER"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
git push origin pr-previews
47 changes: 47 additions & 0 deletions .github/workflows/pr-preview-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Publish CA PR preview

on:
workflow_run:
workflows: [Build CA PR preview]
types: [completed]

permissions:
actions: read
contents: write

concurrency:
group: ca-pr-preview-publish

jobs:
publish:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: pr-previews
- uses: actions/download-artifact@v4
with:
name: ca-pr-preview
path: artifact
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Publish stable PR installer URL
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
PR_NUMBER="$(jq -r '.pr' artifact/preview.json)"
ARTIFACT_SHA="$(jq -r '.sha' artifact/preview.json)"
[[ "$PR_NUMBER" =~ ^[0-9]+$ ]]
test "$ARTIFACT_SHA" = "$HEAD_SHA"
rm -rf "pr/$PR_NUMBER"
mkdir -p "pr/$PR_NUMBER"
cp artifact/* "pr/$PR_NUMBER/"
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
git add "pr/$PR_NUMBER"
git diff --cached --quiet && exit 0
git commit -m "preview: publish PR #$PR_NUMBER at ${HEAD_SHA:0:7}"
git push origin pr-previews
Comment thread
elibosley marked this conversation as resolved.
20 changes: 20 additions & 0 deletions docs/PR_PREVIEWS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Pull request preview plugin

Every open pull request gets an installable Community Applications preview at:

```text
https://raw.githubusercontent.com/unraid/community.applications/pr-previews/pr/<PR_NUMBER>/community.applications.plg
```

Paste that URL into **Plugins > Install Plugin** on a test Unraid server. The
preview keeps the normal `community.applications` plugin identity, so it upgrades
the existing installation in place and preserves CA settings. Installing the
released plugin again returns the server to the stable build.

The build runs with read-only repository access. A separate trusted workflow
downloads the completed artifact without executing pull-request code and
publishes it to the `pr-previews` branch. A closed pull request removes its
published files automatically.

Preview builds are test artifacts, not releases. Use them only on a server where
an in-place Community Applications upgrade is acceptable.
1 change: 1 addition & 0 deletions plugins/CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ the packaged plugin (`pkg_build.sh` only ships `source/community.applications/`)

## Unreleased

- Added: Pull requests now produce installable preview plugins at a stable per-PR URL for testing
- Changed: The monthly spotlight banner now ships with the plugin instead of being downloaded, so it appears instantly with no network request
- Added: New "Use Cloudflare CDN" setting to load the application feed from an alternative source; changing it refreshes the feed automatically
- Removed: Automatic fallback to the GitHub-hosted backup application feed
Expand Down
86 changes: 86 additions & 0 deletions scripts/build-pr-preview.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Build an installable Community Applications preview for a pull request.
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PR_NUMBER="${1:?usage: $0 <pr-number> <git-sha> [output-directory]}"
GIT_SHA="${2:?usage: $0 <pr-number> <git-sha> [output-directory]}"
OUTPUT_DIR="${3:-$ROOT/dist/pr-preview}"
SOURCE_DIR="$ROOT/source/community.applications"
PLUGIN_TEMPLATE="$ROOT/plugins/community.applications.plg"
SHORT_SHA="${GIT_SHA:0:7}"
VERSION="$(date -u +%Y.%m.%d)-pr${PR_NUMBER}-${SHORT_SHA}"
PACKAGE="community.applications-${VERSION}-x86_64-1.txz"
BASE_URL="https://raw.githubusercontent.com/unraid/community.applications/pr-previews/pr/${PR_NUMBER}"

if [[ ! "$PR_NUMBER" =~ ^[0-9]+$ ]]; then
echo "PR number must be numeric: $PR_NUMBER" >&2
exit 1
fi
if [[ ! -d "$SOURCE_DIR" || ! -f "$PLUGIN_TEMPLATE" ]]; then
echo "Run this script from a complete community.applications checkout." >&2
exit 1
fi

rm -rf "$OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
STAGING="$(mktemp -d -t ca-pr-preview.XXXXXX)"
trap 'rm -rf "$STAGING"' EXIT

COPYFILE_DISABLE=1 cp -R "$SOURCE_DIR/" "$STAGING/"
find "$STAGING" \( -name '.DS_Store' -o -name '._*' -o -name 'sftp-config.json' \) -delete
find "$STAGING" -name '.claude' -type d -prune -exec rm -rf {} + 2>/dev/null || true
chmod -R 0755 "$STAGING"

if tar --version 2>/dev/null | grep -q 'GNU tar'; then
tar -C "$STAGING" --owner=0 --group=0 --numeric-owner -cJf "$OUTPUT_DIR/$PACKAGE" .
else
COPYFILE_DISABLE=1 tar -C "$STAGING" --uid 0 --gid 0 --uname root --gname root -cJf "$OUTPUT_DIR/$PACKAGE" .
fi

if command -v md5sum >/dev/null 2>&1; then
MD5="$(md5sum "$OUTPUT_DIR/$PACKAGE" | awk '{print $1}')"
else
MD5="$(md5 -q "$OUTPUT_DIR/$PACKAGE")"
fi

python3 - "$PLUGIN_TEMPLATE" "$OUTPUT_DIR/community.applications.plg" "$VERSION" "$MD5" "$BASE_URL/$PACKAGE" "$BASE_URL/community.applications.plg" <<'PY'
from pathlib import Path
import re
import sys

source, destination, version, md5, package_url, plugin_url = sys.argv[1:]
text = Path(source).read_text()
replacements = {
"version": version,
"md5": md5,
"pluginURL": plugin_url,
}
for entity, value in replacements.items():
text, count = re.subn(
rf'(<!ENTITY\s+{entity}\s+")[^"]*(">)',
rf'\g<1>{value}\g<2>',
text,
count=1,
)
if count != 1:
raise SystemExit(f"could not replace {entity} entity")

text, count = re.subn(
r'(<FILE Name="/boot/config/plugins/&name;/&name;-&version;-x86_64-1\.txz" Run="upgradepkg --install-new --reinstall">\s*<URL>)[^<]*(</URL>)',
rf'\g<1>{package_url}\g<2>',
text,
count=1,
)
if count != 1:
raise SystemExit("could not replace package URL")

Path(destination).write_text(text)
PY

cat > "$OUTPUT_DIR/preview.json" <<EOF
{"pr":${PR_NUMBER},"sha":"${GIT_SHA}","version":"${VERSION}","package":"${PACKAGE}"}
EOF

echo "Built $OUTPUT_DIR/community.applications.plg"
echo "Installer: $BASE_URL/community.applications.plg"
Loading