diff --git a/.github/workflows/pr-plugin-build.yml b/.github/workflows/pr-plugin-build.yml index eee988ea4..12e626aee 100644 --- a/.github/workflows/pr-plugin-build.yml +++ b/.github/workflows/pr-plugin-build.yml @@ -9,7 +9,7 @@ on: permissions: contents: read - pull-requests: write + pull-requests: read actions: read jobs: @@ -109,52 +109,38 @@ jobs: echo "Tarball contents:" tar -tzf ${{ steps.version.outputs.local_txz }} - - name: Configure AWS CLI for R2 + - name: Generate plugin file if: steps.changed-files.outputs.has_changes == 'true' run: | - aws configure set aws_access_key_id ${{ secrets.CLOUDFLARE_PREVIEW_ACCESS_KEY_ID }} - aws configure set aws_secret_access_key ${{ secrets.CLOUDFLARE_PREVIEW_SECRET_ACCESS_KEY }} - aws configure set region auto - - - name: Upload TXZ to R2 - if: steps.changed-files.outputs.has_changes == 'true' - id: upload-txz - run: | - # Upload to R2 with versioned filename to prevent SHA conflicts - aws s3 cp "${{ steps.version.outputs.local_txz }}" \ - "s3://${{ secrets.CLOUDFLARE_PREVIEW_BUCKET_NAME }}/${{ steps.version.outputs.txz_key }}" \ - --endpoint-url "${{ secrets.CLOUDFLARE_S3_URL }}" \ - --acl public-read - - echo "Uploaded TXZ to: ${{ steps.version.outputs.txz_url }}" - - - name: Generate plugin file with R2 URL - if: steps.changed-files.outputs.has_changes == 'true' - run: | - # Local file is non-versioned, but remote URL is versioned - # Pass local filename for SHA calculation and remote filename for download + # 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 }}" \ - "${{ steps.version.outputs.txz_url }}" \ - "${{ steps.version.outputs.plugin_url }}" + "PENDING_UPLOAD" \ + "PENDING_UPLOAD" - - name: Upload PLG to R2 + - name: Save metadata for upload workflow if: steps.changed-files.outputs.has_changes == 'true' - id: upload-plg run: | - # Upload PLG - overwrite existing for updates (consistent filename) - aws s3 cp "${{ steps.version.outputs.plugin_name }}" \ - "s3://${{ secrets.CLOUDFLARE_PREVIEW_BUCKET_NAME }}/${{ steps.version.outputs.plugin_key }}" \ - --endpoint-url "${{ secrets.CLOUDFLARE_S3_URL }}" \ - --acl public-read - - echo "Uploaded PLG to: ${{ steps.version.outputs.plugin_url }}" - - - name: Upload artifacts to GitHub (backup) + 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: @@ -162,72 +148,7 @@ jobs: path: | webgui-pr-*.plg webgui-pr-*.tar.gz + pr-metadata.json + changed_files.txt retention-days: 30 - - name: Format changed files list - if: steps.changed-files.outputs.has_changes == 'true' - id: format-files - run: | - # Format the file list for the comment - echo "files<> $GITHUB_OUTPUT - cat changed_files.txt >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - # Debug output - echo "Changed files found:" - cat changed_files.txt - - - name: Comment on PR - if: steps.changed-files.outputs.has_changes == 'true' - uses: marocchino/sticky-pull-request-comment@v2 - with: - header: pr-plugin - message: | - ## 🔧 PR Test Plugin Available - - A test plugin has been generated for this PR that includes the modified files. - - **Version:** `${{ steps.version.outputs.version }}` - **Build:** [View Workflow Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) - - ### 📥 Installation Instructions: - - **Install via Unraid Web UI:** - 1. Go to **Plugins → Install Plugin** - 2. Copy and paste this URL: - ``` - ${{ steps.version.outputs.plugin_url }} - ``` - 3. Click **Install** - - **Alternative: Direct Download** - - [📦 Download PLG](${{ steps.version.outputs.plugin_url }}) - - [📦 Download TXZ](${{ steps.version.outputs.txz_url }}) - - ### ⚠️ Important Notes: - - - **Testing only:** This plugin is for testing PR changes - - **Backup included:** Original files are automatically backed up - - **Easy removal:** Files are restored when plugin is removed - - **Conflicts:** Remove this plugin before installing production updates - - ### 📝 Modified Files: - -
- Click to expand file list - - ``` - ${{ steps.format-files.outputs.files }} - ``` - -
- - ### 🔄 To Remove: - - Navigate to Plugins → Installed Plugins and remove `webgui-pr-${{ steps.version.outputs.version }}`, or run: - ```bash - plugin remove webgui-pr-${{ steps.version.outputs.version }} - ``` - - --- - 🤖 This comment is automatically generated and will be updated with each new push to this PR. \ No newline at end of file diff --git a/.github/workflows/pr-plugin-upload.yml b/.github/workflows/pr-plugin-upload.yml new file mode 100644 index 000000000..608fb17d2 --- /dev/null +++ b/.github/workflows/pr-plugin-upload.yml @@ -0,0 +1,313 @@ +name: Upload PR Plugin to R2 + +concurrency: + # Use the PR number from the workflow run to group uploads for the same PR + # This ensures previous in-progress uploads for the same PR are cancelled + group: pr-plugin-${{ github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.head_branch }} + cancel-in-progress: true + +on: + workflow_run: + workflows: ["Build PR Plugin"] + types: + - completed + +permissions: + contents: read + pull-requests: write + actions: read + +jobs: + upload-to-r2: + runs-on: ubuntu-latest + # Only run if the build workflow succeeded + if: ${{ github.event.workflow_run.conclusion == 'success' }} + defaults: + run: + shell: bash + env: + SHELLOPTS: errexit:pipefail + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # SECURITY: Always checkout the default branch (trusted code) + # Never checkout PR code in workflow_run context + ref: ${{ github.event.repository.default_branch }} + # Ensure we're checking out the base repository, not a fork + repository: ${{ github.repository }} + + - name: Prepare artifact extraction directory + run: | + set -Eeuo pipefail + IFS=$'\n\t' + mkdir -p "${{ runner.temp }}/artifacts/" + + - name: Download artifacts from build workflow + uses: actions/github-script@v7 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: ${{ github.event.workflow_run.id }}, + }); + + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name.startsWith('webgui-pr-plugin-') + })[0]; + + if (!matchArtifact) { + core.setFailed('No artifacts found from build workflow'); + return; + } + + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + + let fs = require('fs'); + // Write to secure temp location + const zipPath = process.env['RUNNER_TEMP'] + '/artifacts/artifacts.zip'; + fs.writeFileSync(zipPath, Buffer.from(download.data)); + + core.setOutput('artifact_name', matchArtifact.name); + + - name: Extract artifacts + run: | + set -Eeuo pipefail + IFS=$'\n\t' + mkdir -p "${{ runner.temp }}/artifacts/unpacked" + + # Validate archive contents before extraction + bsdtar -tf "${{ runner.temp }}/artifacts/artifacts.zip" | awk ' + /^-/ {next} + { + if ($0 ~ /^\// || $0 ~ /\.\.\//) { print "INVALID:"$0 > "/dev/stderr"; exit 1 } + } + ' + + # Safe extraction with path normalization + bsdtar -xpf "${{ runner.temp }}/artifacts/artifacts.zip" -C "${{ runner.temp }}/artifacts/unpacked" --no-same-owner --no-same-permissions + ls -la "${{ runner.temp }}/artifacts/unpacked" + + # Check if metadata exists + if [ ! -f "${{ runner.temp }}/artifacts/unpacked/pr-metadata.json" ]; then + echo "No metadata file found, build may not have produced any changes" + echo "has_artifacts=false" >> "$GITHUB_ENV" + exit 0 + fi + + echo "has_artifacts=true" >> "$GITHUB_ENV" + + # Validate metadata schema + echo "Metadata present; proceeding with schema validation." + + - name: Parse metadata + if: env.has_artifacts == 'true' + id: metadata + run: | + set -Eeuo pipefail + IFS=$'\n\t' + # Extract values from metadata + PR_NUMBER=$(jq -r '.pr_number' "${{ runner.temp }}/artifacts/unpacked/pr-metadata.json") + VERSION=$(jq -r '.version' "${{ runner.temp }}/artifacts/unpacked/pr-metadata.json") + PR_VERSION=$(jq -r '.pr_version' "${{ runner.temp }}/artifacts/unpacked/pr-metadata.json") + LOCAL_TXZ=$(jq -r '.local_txz' "${{ runner.temp }}/artifacts/unpacked/pr-metadata.json") + REMOTE_TXZ=$(jq -r '.remote_txz' "${{ runner.temp }}/artifacts/unpacked/pr-metadata.json") + PLUGIN_NAME=$(jq -r '.plugin_name' "${{ runner.temp }}/artifacts/unpacked/pr-metadata.json") + + # Generate R2 URLs and keys + S3_BASE_URL="${{ secrets.CLOUDFLARE_PREVIEW_BUCKET_BASE_URL }}/pr-plugins/pr-${PR_NUMBER}" + TXZ_URL="${S3_BASE_URL}/${REMOTE_TXZ}" + PLUGIN_URL="${S3_BASE_URL}/${PLUGIN_NAME}" + TXZ_KEY="pr-plugins/pr-${PR_NUMBER}/${REMOTE_TXZ}" + PLUGIN_KEY="pr-plugins/pr-${PR_NUMBER}/${PLUGIN_NAME}" + + # Output for next steps + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "pr_version=$PR_VERSION" >> $GITHUB_OUTPUT + echo "local_txz=$LOCAL_TXZ" >> $GITHUB_OUTPUT + echo "remote_txz=$REMOTE_TXZ" >> $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 + + # Also extract changed files for comment (limit to 100 files) + jq -r '.changed_files[:100][]' "${{ runner.temp }}/artifacts/unpacked/pr-metadata.json" > "${{ runner.temp }}/artifacts/unpacked/changed_files.txt" + FILE_COUNT=$(jq '.changed_files | length' "${{ runner.temp }}/artifacts/unpacked/pr-metadata.json") + if [ "$FILE_COUNT" -gt 100 ]; then + echo "Note: Showing first 100 of $FILE_COUNT changed files" + echo "truncated=true" >> $GITHUB_OUTPUT + else + echo "truncated=false" >> $GITHUB_OUTPUT + fi + + - name: Upload TXZ to R2 + if: env.has_artifacts == 'true' + env: + LOCAL_TXZ: ${{ steps.metadata.outputs.local_txz }} + TXZ_KEY: ${{ steps.metadata.outputs.txz_key }} + CLOUDFLARE_PREVIEW_BUCKET_NAME: ${{ secrets.CLOUDFLARE_PREVIEW_BUCKET_NAME }} + CLOUDFLARE_S3_URL: ${{ secrets.CLOUDFLARE_S3_URL }} + TXZ_URL: ${{ steps.metadata.outputs.txz_url }} + AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_PREVIEW_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_PREVIEW_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: true + AWS_SHARED_CREDENTIALS_FILE: /dev/null + AWS_CONFIG_FILE: /dev/null + run: | + set -Eeuo pipefail + IFS=$'\n\t' + # Copy from temp directory to working directory + cp "${{ runner.temp }}/artifacts/unpacked/$LOCAL_TXZ" "./" + + # Upload to R2 with versioned filename + aws s3 cp "$LOCAL_TXZ" \ + "s3://$CLOUDFLARE_PREVIEW_BUCKET_NAME/$TXZ_KEY" \ + --endpoint-url "$CLOUDFLARE_S3_URL" \ + --acl public-read + + echo "Uploaded TXZ to: $TXZ_URL" + + - name: Regenerate plugin file with correct R2 URLs + if: env.has_artifacts == 'true' + env: + VERSION: ${{ steps.metadata.outputs.version }} + PR_NUMBER: ${{ steps.metadata.outputs.pr_number }} + PR_VERSION: ${{ steps.metadata.outputs.pr_version }} + LOCAL_TXZ: ${{ steps.metadata.outputs.local_txz }} + REMOTE_TXZ: ${{ steps.metadata.outputs.remote_txz }} + TXZ_URL: ${{ steps.metadata.outputs.txz_url }} + PLUGIN_URL: ${{ steps.metadata.outputs.plugin_url }} + run: | + set -Eeuo pipefail + IFS=$'\n\t' + # Regenerate the plugin with the actual R2 URLs + bash .github/scripts/generate-pr-plugin.sh \ + "$VERSION" \ + "$PR_NUMBER" \ + "$(echo "$PR_VERSION" | cut -d. -f3)" \ + "$LOCAL_TXZ" \ + "$REMOTE_TXZ" \ + "$TXZ_URL" \ + "$PLUGIN_URL" + + - name: Upload PLG to R2 + if: env.has_artifacts == 'true' + env: + PLUGIN_NAME: ${{ steps.metadata.outputs.plugin_name }} + PLUGIN_KEY: ${{ steps.metadata.outputs.plugin_key }} + CLOUDFLARE_PREVIEW_BUCKET_NAME: ${{ secrets.CLOUDFLARE_PREVIEW_BUCKET_NAME }} + CLOUDFLARE_S3_URL: ${{ secrets.CLOUDFLARE_S3_URL }} + PLUGIN_URL: ${{ steps.metadata.outputs.plugin_url }} + AWS_ACCESS_KEY_ID: ${{ secrets.CLOUDFLARE_PREVIEW_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CLOUDFLARE_PREVIEW_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: true + AWS_SHARED_CREDENTIALS_FILE: /dev/null + AWS_CONFIG_FILE: /dev/null + run: | + set -Eeuo pipefail + IFS=$'\n\t' + # Upload PLG - overwrite existing for updates + aws s3 cp "$PLUGIN_NAME" \ + "s3://$CLOUDFLARE_PREVIEW_BUCKET_NAME/$PLUGIN_KEY" \ + --endpoint-url "$CLOUDFLARE_S3_URL" \ + --acl public-read + + echo "Uploaded PLG to: $PLUGIN_URL" + + - name: Format changed files list + if: env.has_artifacts == 'true' + id: format-files + run: | + set -Eeuo pipefail + IFS=$'\n\t' + # Format the file list for the comment with random delimiter + DELIM="FILES_$(openssl rand -hex 8)" + { + echo "files<<$DELIM" + cat "${{ runner.temp }}/artifacts/unpacked/changed_files.txt" + echo "$DELIM" + } >> "$GITHUB_OUTPUT" + + - name: Get PR info + if: env.has_artifacts == 'true' + id: pr-info + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ steps.metadata.outputs.pr_number }} + with: + script: | + const pr_number = parseInt(process.env.PR_NUMBER); + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr_number + }); + core.setOutput('pr_number', pr_number); + + - name: Comment on PR + if: env.has_artifacts == 'true' + uses: marocchino/sticky-pull-request-comment@v2 + with: + number: ${{ steps.pr-info.outputs.pr_number }} + header: pr-plugin + message: | + ## 🔧 PR Test Plugin Available + + A test plugin has been generated for this PR that includes the modified files. + + **Version:** `${{ steps.metadata.outputs.version }}` + **Build:** [View Workflow Run](${{ github.event.workflow_run.html_url }}) + + ### 📥 Installation Instructions: + + **Install via Unraid Web UI:** + 1. Go to **Plugins → Install Plugin** + 2. Copy and paste this URL: + ``` + ${{ steps.metadata.outputs.plugin_url }} + ``` + 3. Click **Install** + + **Alternative: Direct Download** + - [📦 Download PLG](${{ steps.metadata.outputs.plugin_url }}) + - [📦 Download TXZ](${{ steps.metadata.outputs.txz_url }}) + + ### ⚠️ Important Notes: + + - **Testing only:** This plugin is for testing PR changes + - **Backup included:** Original files are automatically backed up + - **Easy removal:** Files are restored when plugin is removed + - **Conflicts:** Remove this plugin before installing production updates + + ### 📝 Modified Files: + +
+ Click to expand file list + + ``` + ${{ steps.format-files.outputs.files }} + ``` + +
+ + ### 🔄 To Remove: + + Navigate to Plugins → Installed Plugins and remove `webgui-pr-${{ steps.metadata.outputs.version }}`, or run: + ```bash + plugin remove webgui-pr-${{ steps.metadata.outputs.version }} + ``` + + --- + 🤖 This comment is automatically generated and will be updated with each new push to this PR.