From 4a8cdf0fe7bae1823b959578cfbe19bb2f1fea39 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 20 May 2026 11:00:26 -0400 Subject: [PATCH 1/4] fix(ci): support forked plugin publishing Purpose of the change: - Allow forked pull request builds to publish preview plugins through a trusted workflow path. How behavior was before: - The pull_request build attempted to upload directly to Cloudflare R2 from the reusable plugin build workflow. - Forked builds could not access Cloudflare secrets, leaving --endpoint-url empty and causing the aws command to fail. Why that was a problem: - Network/plugin preview artifacts were built but never uploaded to Cloudflare for forked PRs. What the new change accomplishes: - Pull request builds upload plugin files as GitHub artifacts only. - A workflow_run job running with base repository permissions downloads, validates, uploads, and comments the preview URL. How it works: - The direct Cloudflare upload step is skipped for pull_request events. - The new upload-pr-plugin workflow finds the successful CI run artifact, validates .plg/.txz contents, syncs them to the PR preview path, and posts a sticky PR comment. - Direct push uploads now quote and validate Cloudflare endpoint and bucket inputs. --- .github/workflows/build-plugin.yml | 54 +++---- .github/workflows/upload-pr-plugin.yml | 194 +++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/upload-pr-plugin.yml diff --git a/.github/workflows/build-plugin.yml b/.github/workflows/build-plugin.yml index f7547bcce7..74a7e05acb 100644 --- a/.github/workflows/build-plugin.yml +++ b/.github/workflows/build-plugin.yml @@ -38,13 +38,13 @@ on: description: "Whether to automatically trigger the release-production workflow (default: false)" secrets: CF_ACCESS_KEY_ID: - required: true + required: false CF_SECRET_ACCESS_KEY: - required: true + required: false CF_BUCKET_PREVIEW: - required: true + required: false CF_ENDPOINT: - required: true + required: false UNRAID_BOT_GITHUB_ADMIN_TOKEN: required: false jobs: @@ -144,7 +144,7 @@ jobs: - name: Upload to GHA uses: actions/upload-artifact@v6 with: - name: unraid-plugin-${{ github.run_id }}-${{ inputs.RELEASE_TAG }} + name: unraid-plugin-${{ github.run_id }}-${{ inputs.TAG || inputs.RELEASE_TAG || 'build' }} path: plugin/deploy/ - name: Upload Release Assets @@ -168,33 +168,30 @@ jobs: token: ${{ secrets.UNRAID_BOT_GITHUB_ADMIN_TOKEN }} - name: Upload to Cloudflare - if: inputs.RELEASE_CREATED == 'false' + if: inputs.RELEASE_CREATED == 'false' && github.event_name != 'pull_request' env: AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: true + AWS_SHARED_CREDENTIALS_FILE: /dev/null + AWS_CONFIG_FILE: /dev/null + CF_BUCKET_PREVIEW: ${{ secrets.CF_BUCKET_PREVIEW }} + CF_ENDPOINT: ${{ secrets.CF_ENDPOINT }} + BUCKET_PATH: ${{ inputs.BUCKET_PATH }} run: | + : "${CF_BUCKET_PREVIEW:?CF_BUCKET_PREVIEW secret is required}" + : "${CF_ENDPOINT:?CF_ENDPOINT secret is required}" + : "${BUCKET_PATH:?BUCKET_PATH input is required}" + # Sync the deploy directory to the Cloudflare bucket with explicit content encoding and public-read ACL - aws s3 sync deploy/ s3://${{ secrets.CF_BUCKET_PREVIEW }}/${{ inputs.BUCKET_PATH }} \ - --endpoint-url ${{ secrets.CF_ENDPOINT }} \ + aws s3 sync deploy/ "s3://${CF_BUCKET_PREVIEW}/${BUCKET_PATH}" \ + --endpoint-url "${CF_ENDPOINT}" \ --checksum-algorithm CRC32 \ --no-guess-mime-type \ --content-encoding none \ --acl public-read - - name: Comment URL - if: github.event_name == 'pull_request' - uses: thollander/actions-comment-pull-request@v3 - with: - comment-tag: prlink - mode: recreate - message: | - This plugin has been deployed to Cloudflare R2 and is available for testing. - Download it at this URL: - ``` - ${{ inputs.BASE_URL }}/tag/${{ inputs.TAG }}/dynamix.unraid.net.plg - ``` - - name: Clean up old preview builds if: inputs.RELEASE_CREATED == 'false' && github.event_name == 'push' continue-on-error: true @@ -202,16 +199,23 @@ jobs: AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: true + AWS_SHARED_CREDENTIALS_FILE: /dev/null + AWS_CONFIG_FILE: /dev/null + CF_BUCKET_PREVIEW: ${{ secrets.CF_BUCKET_PREVIEW }} + CF_ENDPOINT: ${{ secrets.CF_ENDPOINT }} run: | echo "๐Ÿงน Cleaning up old preview builds (keeping last 7 days)..." + : "${CF_BUCKET_PREVIEW:?CF_BUCKET_PREVIEW secret is required}" + : "${CF_ENDPOINT:?CF_ENDPOINT secret is required}" # Calculate cutoff date (7 days ago) CUTOFF_DATE=$(date -d "7 days ago" +"%Y.%m.%d") echo "Deleting builds older than: ${CUTOFF_DATE}" # List and delete old timestamped .txz files - OLD_FILES=$(aws s3 ls "s3://${{ secrets.CF_BUCKET_PREVIEW }}/unraid-api/" \ - --endpoint-url ${{ secrets.CF_ENDPOINT }} --recursive | \ + OLD_FILES=$(aws s3 ls "s3://${CF_BUCKET_PREVIEW}/unraid-api/" \ + --endpoint-url "${CF_ENDPOINT}" --recursive | \ grep -E "dynamix\.unraid\.net-[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]{4}\.txz" | \ awk '{print $4}' || true) @@ -222,8 +226,8 @@ jobs: FILE_DATE="${BASH_REMATCH[1]}" if [[ "$FILE_DATE" < "$CUTOFF_DATE" ]]; then echo "Deleting old build: $(basename "$file")" - aws s3 rm "s3://${{ secrets.CF_BUCKET_PREVIEW }}/${file}" \ - --endpoint-url ${{ secrets.CF_ENDPOINT }} || true + aws s3 rm "s3://${CF_BUCKET_PREVIEW}/${file}" \ + --endpoint-url "${CF_ENDPOINT}" || true ((DELETED_COUNT++)) fi fi diff --git a/.github/workflows/upload-pr-plugin.yml b/.github/workflows/upload-pr-plugin.yml new file mode 100644 index 0000000000..5b94d74ec9 --- /dev/null +++ b/.github/workflows/upload-pr-plugin.yml @@ -0,0 +1,194 @@ +name: Upload PR Plugin to Cloudflare + +on: + workflow_run: + workflows: ["CI - Main (API)"] + types: + - completed + +permissions: + actions: read + contents: read + pull-requests: write + +concurrency: + group: pr-plugin-upload-${{ github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.head_branch }} + cancel-in-progress: true + +jobs: + upload-pr-plugin: + if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }} + runs-on: ubuntu-latest + defaults: + run: + shell: bash + env: + SHELLOPTS: errexit:pipefail + + steps: + - name: Resolve workflow run context + id: context + uses: actions/github-script@v7 + with: + script: | + const workflowRun = context.payload.workflow_run; + const runId = workflowRun.id; + let prNumber = workflowRun.pull_requests?.[0]?.number; + + if (!prNumber) { + const prs = await github.rest.repos.listPullRequestsAssociatedWithCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + commit_sha: workflowRun.head_sha, + }); + prNumber = prs.data[0]?.number; + } + + if (!prNumber) { + core.setFailed(`Unable to resolve a pull request for workflow run ${runId}`); + return; + } + + core.setOutput('run_id', runId); + core.setOutput('pr_number', prNumber); + + - name: Prepare artifact directory + run: | + set -Eeuo pipefail + IFS=$'\n\t' + mkdir -p "${{ runner.temp }}/pr-plugin" + + - name: Download plugin artifact + uses: actions/github-script@v7 + env: + RUN_ID: ${{ steps.context.outputs.run_id }} + with: + script: | + const fs = require('fs'); + const path = require('path'); + const runId = Number(process.env.RUN_ID); + + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: runId, + }); + + const artifact = artifacts.data.artifacts.find((candidate) => + candidate.name.startsWith(`unraid-plugin-${runId}-`) + ); + + if (!artifact) { + core.setFailed(`No unraid-plugin artifact found for workflow run ${runId}`); + return; + } + + const download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: artifact.id, + archive_format: 'zip', + }); + + const zipPath = path.join(process.env.RUNNER_TEMP, 'pr-plugin', 'artifact.zip'); + fs.writeFileSync(zipPath, Buffer.from(download.data)); + core.info(`Downloaded ${artifact.name}`); + + - name: Extract and validate plugin files + id: files + run: | + set -Eeuo pipefail + IFS=$'\n\t' + + ZIP_PATH="${{ runner.temp }}/pr-plugin/artifact.zip" + UNPACKED_DIR="${{ runner.temp }}/pr-plugin/unpacked" + DEPLOY_DIR="${{ runner.temp }}/pr-plugin/deploy" + mkdir -p "$UNPACKED_DIR" "$DEPLOY_DIR" + + unzip -l "$ZIP_PATH" | awk ' + NR <= 3 || /^-/ || /^Archive:/ {next} + /files$/ {exit} + { + filename = $NF + if (filename ~ /^\// || filename ~ /\.\.\//) { + print "Invalid artifact path: " filename > "/dev/stderr" + exit 1 + } + } + ' + + unzip -o "$ZIP_PATH" -d "$UNPACKED_DIR" + + while IFS= read -r file; do + filename=$(basename "$file") + case "$filename" in + *.plg|*.txz) ;; + *) + echo "Unexpected artifact file: $file" + exit 1 + ;; + esac + + if [ -e "$DEPLOY_DIR/$filename" ]; then + echo "Duplicate artifact filename: $filename" + exit 1 + fi + + cp "$file" "$DEPLOY_DIR/$filename" + done < <(find "$UNPACKED_DIR" -type f) + + PLG_COUNT=$(find "$DEPLOY_DIR" -maxdepth 1 -name "*.plg" -type f | wc -l | tr -d ' ') + TXZ_COUNT=$(find "$DEPLOY_DIR" -maxdepth 1 -name "*.txz" -type f | wc -l | tr -d ' ') + + if [ "$PLG_COUNT" -ne 1 ]; then + echo "Expected exactly one .plg file, found $PLG_COUNT" + exit 1 + fi + + if [ "$TXZ_COUNT" -lt 1 ]; then + echo "Expected at least one .txz file, found $TXZ_COUNT" + exit 1 + fi + + echo "deploy_dir=$DEPLOY_DIR" >> "$GITHUB_OUTPUT" + ls -la "$DEPLOY_DIR" + + - name: Upload plugin to Cloudflare + env: + AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: true + AWS_SHARED_CREDENTIALS_FILE: /dev/null + AWS_CONFIG_FILE: /dev/null + CF_BUCKET_PREVIEW: ${{ secrets.CF_BUCKET_PREVIEW }} + CF_ENDPOINT: ${{ secrets.CF_ENDPOINT }} + DEPLOY_DIR: ${{ steps.files.outputs.deploy_dir }} + PR_NUMBER: ${{ steps.context.outputs.pr_number }} + run: | + set -Eeuo pipefail + IFS=$'\n\t' + + : "${CF_BUCKET_PREVIEW:?CF_BUCKET_PREVIEW secret is required}" + : "${CF_ENDPOINT:?CF_ENDPOINT secret is required}" + : "${DEPLOY_DIR:?DEPLOY_DIR is required}" + : "${PR_NUMBER:?PR_NUMBER is required}" + + aws s3 sync "$DEPLOY_DIR/" "s3://${CF_BUCKET_PREVIEW}/unraid-api/tag/PR${PR_NUMBER}" \ + --endpoint-url "$CF_ENDPOINT" \ + --checksum-algorithm CRC32 \ + --no-guess-mime-type \ + --content-encoding none \ + --acl public-read + + - name: Comment URL + uses: marocchino/sticky-pull-request-comment@v2 + with: + number: ${{ steps.context.outputs.pr_number }} + header: prlink + message: | + This plugin has been deployed to Cloudflare R2 and is available for testing. + Download it at this URL: + ``` + https://preview.dl.unraid.net/unraid-api/tag/PR${{ steps.context.outputs.pr_number }}/dynamix.unraid.net.plg + ``` From 81cd03cad7992f0fbc5ca33dd7e6ad86b1103931 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 20 May 2026 11:03:35 -0400 Subject: [PATCH 2/4] fix(ci): harden PR plugin closeout Purpose of the change: - Make the merged-PR plugin closeout workflow reliable for forked and non-plugin PRs. How behavior was before: - The workflow ran on pull_request closed events and tried to use Cloudflare secrets directly. - It failed when no successful plugin artifact existed for the PR. - Fork-originated runs had no secret source, so any later R2 operation would fail with empty Cloudflare values. Why that was a problem: - Closed or merged PRs produced noisy workflow failures even when there was no PR plugin to close out. - Forked PR plugin redirects could not safely reach Cloudflare using the pull_request event context. What the new change accomplishes: - Runs merged-PR closeout from pull_request_target so trusted workflow code can access repository secrets. - Checks for the existing PR plugin in Cloudflare R2 and exits successfully when there is nothing to close out. - Rewrites and uploads the staging redirect only when an actual PR plugin exists. How it works: - The workflow validates the PR number and expected R2 key format. - It downloads the existing PR plugin from R2, bumps its version, changes the plugin URL to staging, clears old PR artifacts, then uploads the redirect plugin. - R2 commands now use quoted endpoint values, isolated AWS config, and explicit secret validation. --- .../workflows/push-staging-pr-on-close.yml | 124 ++++++++++++++---- 1 file changed, 97 insertions(+), 27 deletions(-) diff --git a/.github/workflows/push-staging-pr-on-close.yml b/.github/workflows/push-staging-pr-on-close.yml index d536d29ef1..7cabaa6f9e 100644 --- a/.github/workflows/push-staging-pr-on-close.yml +++ b/.github/workflows/push-staging-pr-on-close.yml @@ -6,7 +6,7 @@ name: Replace PR Plugin with Staging Redirect on Merge # update to the staging version on their next update check. on: - pull_request: + pull_request_target: types: - closed workflow_dispatch: @@ -23,40 +23,84 @@ on: jobs: push-staging-redirect: - if: (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || (github.event_name == 'workflow_dispatch' && inputs.pr_merged == true) + if: (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) || (github.event_name == 'workflow_dispatch' && inputs.pr_merged == true) runs-on: ubuntu-latest permissions: contents: read actions: read + pull-requests: write steps: - name: Set PR number id: pr_number run: | - if [ "${{ github.event_name }}" == "pull_request" ]; then - echo "pr_number=${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT + set -Eeuo pipefail + IFS=$'\n\t' + + if [ "${{ github.event_name }}" = "pull_request_target" ]; then + PR_NUMBER="${{ github.event.pull_request.number }}" else - echo "pr_number=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT + PR_NUMBER="${{ inputs.pr_number }}" fi - - name: Download artifact - uses: dawidd6/action-download-artifact@v11 - with: - name_is_regexp: true - name: unraid-plugin-.* - path: connect-files - pr: ${{ steps.pr_number.outputs.pr_number }} - workflow: main.yml - workflow_conclusion: success - search_artifacts: true - if_no_artifact_found: fail + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "Error: PR number '$PR_NUMBER' is not numeric" >&2 + exit 1 + fi + + echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" + + - name: Download PR plugin from Cloudflare + id: download_pr_plugin + env: + AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: true + AWS_SHARED_CREDENTIALS_FILE: /dev/null + AWS_CONFIG_FILE: /dev/null + CF_BUCKET_PREVIEW: ${{ secrets.CF_BUCKET_PREVIEW }} + CF_ENDPOINT: ${{ secrets.CF_ENDPOINT }} + PR_NUMBER: ${{ steps.pr_number.outputs.pr_number }} + run: | + set -Eeuo pipefail + IFS=$'\n\t' + + : "${CF_BUCKET_PREVIEW:?CF_BUCKET_PREVIEW secret is required}" + : "${CF_ENDPOINT:?CF_ENDPOINT secret is required}" + : "${PR_NUMBER:?PR_NUMBER is required}" + + PR_PREFIX="unraid-api/tag/PR${PR_NUMBER}" + PLUGIN_KEY="${PR_PREFIX}/dynamix.unraid.net.plg" + + if ! [[ "$PLUGIN_KEY" =~ ^unraid-api/tag/PR[0-9]+/dynamix\.unraid\.net\.plg$ ]]; then + echo "Error: Invalid plugin key '$PLUGIN_KEY'" >&2 + exit 1 + fi + + mkdir -p pr-release + + if ! aws s3api head-object \ + --bucket "$CF_BUCKET_PREVIEW" \ + --key "$PLUGIN_KEY" \ + --endpoint-url "$CF_ENDPOINT" > /dev/null 2>&1; then + echo "No PR plugin found at s3://${CF_BUCKET_PREVIEW}/${PLUGIN_KEY}; nothing to close out." + echo "found=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + aws s3 cp "s3://${CF_BUCKET_PREVIEW}/${PLUGIN_KEY}" pr-release/dynamix.unraid.net.plg \ + --endpoint-url "$CF_ENDPOINT" + + echo "found=true" >> "$GITHUB_OUTPUT" - name: Update Downloaded Plugin to Redirect to Staging + if: steps.download_pr_plugin.outputs.found == 'true' run: | # Find the .plg file in the downloaded artifact - plgfile=$(find connect-files -name "*.plg" -type f | head -1) + plgfile="pr-release/dynamix.unraid.net.plg" if [ ! -f "$plgfile" ]; then - echo "ERROR: .plg file not found in connect-files/" - ls -la connect-files/ + echo "ERROR: .plg file not found in pr-release/" + ls -la pr-release/ exit 1 fi @@ -79,39 +123,65 @@ jobs: echo "Modified plugin to redirect to: ${url}" echo "Version bumped from ${current_version} to ${new_version}" - - mkdir -p pr-release - mv "${plgfile}" pr-release/dynamix.unraid.net.plg - name: Clean up old PR artifacts from Cloudflare + if: steps.download_pr_plugin.outputs.found == 'true' env: AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: true + AWS_SHARED_CREDENTIALS_FILE: /dev/null + AWS_CONFIG_FILE: /dev/null + CF_BUCKET_PREVIEW: ${{ secrets.CF_BUCKET_PREVIEW }} + CF_ENDPOINT: ${{ secrets.CF_ENDPOINT }} + PR_NUMBER: ${{ steps.pr_number.outputs.pr_number }} run: | + set -Eeuo pipefail + IFS=$'\n\t' + + : "${CF_BUCKET_PREVIEW:?CF_BUCKET_PREVIEW secret is required}" + : "${CF_ENDPOINT:?CF_ENDPOINT secret is required}" + : "${PR_NUMBER:?PR_NUMBER is required}" + # Delete all existing files in the PR directory first (txz, plg, etc.) - aws s3 rm s3://${{ secrets.CF_BUCKET_PREVIEW }}/unraid-api/tag/PR${{ steps.pr_number.outputs.pr_number }}/ \ + aws s3 rm "s3://${CF_BUCKET_PREVIEW}/unraid-api/tag/PR${PR_NUMBER}/" \ --recursive \ - --endpoint-url ${{ secrets.CF_ENDPOINT }} + --endpoint-url "$CF_ENDPOINT" echo "โœ… Cleaned up old PR artifacts" - name: Upload PR Redirect Plugin to Cloudflare + if: steps.download_pr_plugin.outputs.found == 'true' env: AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: auto + AWS_EC2_METADATA_DISABLED: true + AWS_SHARED_CREDENTIALS_FILE: /dev/null + AWS_CONFIG_FILE: /dev/null + CF_BUCKET_PREVIEW: ${{ secrets.CF_BUCKET_PREVIEW }} + CF_ENDPOINT: ${{ secrets.CF_ENDPOINT }} + PR_NUMBER: ${{ steps.pr_number.outputs.pr_number }} run: | + set -Eeuo pipefail + IFS=$'\n\t' + + : "${CF_BUCKET_PREVIEW:?CF_BUCKET_PREVIEW secret is required}" + : "${CF_ENDPOINT:?CF_ENDPOINT secret is required}" + : "${PR_NUMBER:?PR_NUMBER is required}" + # Upload only the redirect plugin file aws s3 cp pr-release/dynamix.unraid.net.plg \ - s3://${{ secrets.CF_BUCKET_PREVIEW }}/unraid-api/tag/PR${{ steps.pr_number.outputs.pr_number }}/dynamix.unraid.net.plg \ - --endpoint-url ${{ secrets.CF_ENDPOINT }} \ + "s3://${CF_BUCKET_PREVIEW}/unraid-api/tag/PR${PR_NUMBER}/dynamix.unraid.net.plg" \ + --endpoint-url "$CF_ENDPOINT" \ --content-encoding none \ --acl public-read echo "โœ… Uploaded redirect plugin" - name: Output redirect information + if: steps.download_pr_plugin.outputs.found == 'true' run: | echo "โœ… PR plugin replaced with staging redirect version" echo "PR URL remains: https://preview.dl.unraid.net/unraid-api/tag/PR${{ steps.pr_number.outputs.pr_number }}/dynamix.unraid.net.plg" @@ -119,7 +189,7 @@ jobs: echo "Users updating from this PR version will automatically switch to staging" - name: Comment on PR about staging redirect - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request_target' && steps.download_pr_plugin.outputs.found == 'true' uses: thollander/actions-comment-pull-request@v3 with: comment-tag: pr-closed-staging From ca2db18f4f0faa5ae5d6b4cc3051e4dee6ce42a5 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 20 May 2026 12:08:12 -0400 Subject: [PATCH 3/4] fix(ci): address CodeRabbit workflow feedback Purpose of the change: - Address CodeRabbit review feedback on the forked plugin publish and PR plugin closeout workflows. How behavior was before: - New workflow actions used mutable tags. - workflow_run upload concurrency could fall back to a non-unique branch name for forked PRs. - workflow_dispatch input was interpolated directly into the closeout shell script. - The closeout sed expression used a misleading non-greedy-looking pattern. Why that was a problem: - Mutable actions and direct input interpolation are avoidable security risks in workflows that handle repository secrets. - Non-unique fallback concurrency could cancel unrelated forked upload jobs. - The sed pattern was confusing and could invite incorrect future edits. What the new change accomplishes: - Pins the new workflow actions to immutable SHAs. - Uses workflow_run.id as the upload concurrency fallback. - Passes workflow_dispatch PR input through step env before shell validation. - Uses a quoted-string character class for plugin_url rewriting. How it works: - GitHub action refs are replaced with resolved 40-character SHAs. - The PR number selector reads trusted shell variables and keeps numeric validation. - The targeted workflow regexes and YAML parse checks now pass. --- .github/workflows/push-staging-pr-on-close.yml | 14 +++++++++----- .github/workflows/upload-pr-plugin.yml | 8 ++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.github/workflows/push-staging-pr-on-close.yml b/.github/workflows/push-staging-pr-on-close.yml index 7cabaa6f9e..8e4042eb64 100644 --- a/.github/workflows/push-staging-pr-on-close.yml +++ b/.github/workflows/push-staging-pr-on-close.yml @@ -32,14 +32,18 @@ jobs: steps: - name: Set PR number id: pr_number + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_PR_NUMBER: ${{ inputs.pr_number }} + PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} run: | set -Eeuo pipefail IFS=$'\n\t' - if [ "${{ github.event_name }}" = "pull_request_target" ]; then - PR_NUMBER="${{ github.event.pull_request.number }}" + if [ "$EVENT_NAME" = "pull_request_target" ]; then + PR_NUMBER="$PULL_REQUEST_NUMBER" else - PR_NUMBER="${{ inputs.pr_number }}" + PR_NUMBER="$INPUT_PR_NUMBER" fi if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then @@ -119,7 +123,7 @@ jobs: # Change the plugin url to point to staging - users will switch to staging on next update url="https://preview.dl.unraid.net/unraid-api/dynamix.unraid.net.plg" - sed -i -E "s#()#\1${url}\2#g" "${plgfile}" || exit 1 + sed -i -E "s#()#\1${url}\2#g" "${plgfile}" || exit 1 echo "Modified plugin to redirect to: ${url}" echo "Version bumped from ${current_version} to ${new_version}" @@ -190,7 +194,7 @@ jobs: - name: Comment on PR about staging redirect if: github.event_name == 'pull_request_target' && steps.download_pr_plugin.outputs.found == 'true' - uses: thollander/actions-comment-pull-request@v3 + uses: thollander/actions-comment-pull-request@24bffb9b452ba05a4f3f77933840a6a841d1b32b with: comment-tag: pr-closed-staging mode: recreate diff --git a/.github/workflows/upload-pr-plugin.yml b/.github/workflows/upload-pr-plugin.yml index 5b94d74ec9..6443c1376f 100644 --- a/.github/workflows/upload-pr-plugin.yml +++ b/.github/workflows/upload-pr-plugin.yml @@ -12,7 +12,7 @@ permissions: pull-requests: write concurrency: - group: pr-plugin-upload-${{ github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.head_branch }} + group: pr-plugin-upload-${{ github.event.workflow_run.pull_requests[0].number || github.event.workflow_run.id }} cancel-in-progress: true jobs: @@ -28,7 +28,7 @@ jobs: steps: - name: Resolve workflow run context id: context - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b with: script: | const workflowRun = context.payload.workflow_run; @@ -59,7 +59,7 @@ jobs: mkdir -p "${{ runner.temp }}/pr-plugin" - name: Download plugin artifact - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b env: RUN_ID: ${{ steps.context.outputs.run_id }} with: @@ -182,7 +182,7 @@ jobs: --acl public-read - name: Comment URL - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 with: number: ${{ steps.context.outputs.pr_number }} header: prlink From dd7ed305038c0c0effb9fae244d0778a611f3956 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 20 May 2026 12:10:22 -0400 Subject: [PATCH 4/4] docs(ci): add CodeRabbit fix checklist Purpose of the change: - Preserve the CodeRabbit fix-loop checklist for PR review traceability. How behavior was before: - The checklist existed only as a local untracked file. Why that was a problem: - Reviewers could not see the item-by-item evidence for resolved CodeRabbit feedback. What the new change accomplishes: - Adds the completed WIP checklist under .codex for the PR branch. How it works: - Records the reviewed items, statuses, validation commands, final unresolved-thread check, and resolved thread IDs. --- .codex/coderabbit-fixes-wip.md | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .codex/coderabbit-fixes-wip.md diff --git a/.codex/coderabbit-fixes-wip.md b/.codex/coderabbit-fixes-wip.md new file mode 100644 index 0000000000..2a771a01b1 --- /dev/null +++ b/.codex/coderabbit-fixes-wip.md @@ -0,0 +1,69 @@ +# CodeRabbit Fixes WIP + +## Context + +- Repo: unraid/api +- Branch: codex/fix-forked-plugin-publish +- PR: 2014 +- PR URL: https://github.com/unraid/api/pull/2014 +- Generated at: 2026-05-20T16:03:41Z + +## Inputs Pulled + +- [x] Unresolved CodeRabbit review threads pulled +- [x] Top-level CodeRabbit review notes pulled +- [x] Top-level actionable review-body comments extracted into queue + +## Fix Queue + +| Item ID | Type | File | Line | Summary | Status | Link | Evidence | +| --- | --- | --- | --- | --- | --- | --- | --- | +| CR-001 | thread | .github/workflows/upload-pr-plugin.yml | 31 | Pin mutable `uses:` action refs to immutable SHAs. | DONE | https://github.com/unraid/api/pull/2014#discussion_r3275010283 | `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/upload-pr-plugin.yml")'`; `rg -nP '^\\s*uses:\\s*[^@]+@(?!(?:[a-f0-9]{40})$).+' .github/workflows/upload-pr-plugin.yml` returned no matches. | +| EXT-001 | thread | .github/workflows/upload-pr-plugin.yml | 15 | Make workflow_run concurrency fallback unique when PR number is unavailable. | DONE | https://github.com/unraid/api/pull/2014#discussion_r3275030567 | `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/upload-pr-plugin.yml")'`. | +| CR-002 | thread | .github/workflows/push-staging-pr-on-close.yml | 50 | Avoid direct template interpolation of `inputs.pr_number` in shell. | DONE | https://github.com/unraid/api/pull/2014#discussion_r3275031746 | `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/push-staging-pr-on-close.yml")'`; direct `inputs.pr_number` use is limited to step `env`. | +| RVW-001 | review-body | top-level | n/a | Top-level review repeats CR-001 action pinning request. | DONE | https://github.com/unraid/api/pull/2014#pullrequestreview-4329727578 | Covered by CR-001. | +| RVW-002 | review-body | top-level | n/a | Top-level review repeats CR-002 and notes misleading `sed` `.*?` regex. | DONE | https://github.com/unraid/api/pull/2014#pullrequestreview-4329754125 | `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/push-staging-pr-on-close.yml")'`; `rg -n '\\.\\*\\?' .github/workflows/push-staging-pr-on-close.yml` returned no matches. | +| EXT-002 | follow-up | .github/workflows/push-staging-pr-on-close.yml | 197 | Pin close-out comment action because workflow now runs in privileged `pull_request_target` context. | DONE | n/a | `rg -nP '^\\s*uses:\\s*[^@]+@(?!(?:[a-f0-9]{40})$).+' .github/workflows/upload-pr-plugin.yml .github/workflows/push-staging-pr-on-close.yml` returned no matches. | + +## Execution Log + +### 1. Item: CR-001 +- Action: Pin new upload workflow action refs to full SHAs. +- Validation: `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/upload-pr-plugin.yml")'`; `rg -nP '^\s*uses:\s*[^@]+@(?!(?:[a-f0-9]{40})$).+' .github/workflows/upload-pr-plugin.yml`. +- Result: Passed; no mutable action refs remain in `.github/workflows/upload-pr-plugin.yml`. + +### 2. Item: EXT-001 +- Action: Use `github.event.workflow_run.id` as the no-PR-number concurrency fallback. +- Validation: `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/upload-pr-plugin.yml")'`. +- Result: Passed; fallback is unique per workflow run. + +### 3. Item: CR-002 +- Action: Move workflow_dispatch PR number into a step environment variable and read it as shell data before validation. +- Validation: `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/push-staging-pr-on-close.yml")'`; inspected direct `inputs.pr_number` occurrences. +- Result: Passed; the user-controlled input is no longer interpolated directly into the shell script body. + +### 4. Item: RVW-001 +- Action: Marked duplicate top-level review-body item as covered by CR-001. +- Validation: Same evidence as CR-001. +- Result: Done. + +### 5. Item: RVW-002 +- Action: Replace misleading `.*?` sed pattern with a quoted-string character class. +- Validation: `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/push-staging-pr-on-close.yml")'`; `rg -n '\.\*\?' .github/workflows/push-staging-pr-on-close.yml`. +- Result: Passed; no misleading `.*?` remains. + +### 6. Item: EXT-002 +- Action: Pin `thollander/actions-comment-pull-request` to the current `v3` SHA. +- Validation: `ruby -e 'require "yaml"; ARGV.each { |f| YAML.load_file(f) }' .github/workflows/upload-pr-plugin.yml .github/workflows/push-staging-pr-on-close.yml`; targeted mutable-action scan. +- Result: Passed; no mutable action refs remain in the two changed workflows. + +## Final Checks + +- [x] Queue reviewed: no `TODO` left +- [x] Remaining `BLOCKED` items documented with reason +- [x] Re-pulled CodeRabbit threads and reviews +- [x] No unhandled top-level review-body comment remains + +Final evidence: +- `coderabbit-review-data final 2014` returned `unresolved_coderabbit_threads 0`. +- Resolved fixed review threads `PRRT_kwDOC2VmQM6DhuxC`, `PRRT_kwDOC2VmQM6DhyZ8`, and `PRRT_kwDOC2VmQM6Dhym0`.