From 2282931f7049a1e147cfedde21c1486a6d9b88ae Mon Sep 17 00:00:00 2001 From: neveler <55753029+neveler@users.noreply.github.com> Date: Fri, 31 Oct 2025 23:11:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E4=B8=BA=20PR=20=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=A2=84=E8=A7=88=E6=9E=84=E5=BB=BA=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Burning_TNT --- .github/workflows/pr-preview.yml | 384 +++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 .github/workflows/pr-preview.yml diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..7e9cd31 --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,384 @@ +name: PR Preview +on: + issue_comment: + types: [created] + pull_request_target: + types: [labeled, synchronize, closed] +concurrency: + group: pr-preview + cancel-in-progress: false +env: + GITHUB_PR_NUMBER: ${{ github.event_name == 'issue_comment' && github.event.issue.number || github.event.pull_request.number }} + JEKYLL_ENV: production +jobs: + preview-remove: + if: >- + ${{ + !github.event.repository.fork && ( + ( github.event_name == 'pull_request_target' && ( + github.event.action == 'closed' || ( + github.event.action == 'labeled' && + github.event.label.name == 'preview/remove' + ) + ) + ) || ( + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + github.event.comment.body == '/preview remove' && + contains('OWNER,MEMBER', github.event.comment.author_association) + ) + ) + }} + runs-on: ubuntu-latest + permissions: + actions: write + contents: write + pull-requests: write + steps: + - id: restore-pages + name: Restore Pages + uses: actions/cache/restore@v4 + with: + key: pages-${{ github.run_id }} + path: /home/runner/gh-pages + restore-keys: pages- + - name: Prepare Pages + run: | + mkdir -p /home/runner/gh-pages/data/{files,refs} + rm -rf /home/runner/gh-pages/PR${{ env.GITHUB_PR_NUMBER }} + grep -rlw 'PR${{ env.GITHUB_PR_NUMBER }}' /home/runner/gh-pages/data/refs | xargs -r sed -i '/\/d' + for ref in /home/runner/gh-pages/data/refs/*; do + if [ ! -s "$ref" ]; then + base=$(basename "$ref") + rm -f "/home/runner/gh-pages/data/files/$base" + rm -f "$ref" + fi + done + echo "[$(date)] Remove Preview PR${{ env.GITHUB_PR_NUMBER }}" > /home/runner/gh-pages/index.html + - name: Save Pages + uses: actions/cache/save@v4 + with: + key: pages-${{ github.run_id }} + path: /home/runner/gh-pages + - if: ${{ github.event_name == 'issue_comment' }} + id: comment-message + name: Comment Message (issue_comment) + uses: actions/github-script@v8 + with: + script: | + github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + issue_number: context.issue.number, + body: "[Preview] Preview has been removed." + }); + - if: ${{ github.event_name == 'pull_request_target' || steps.comment-message.outcome == 'failure' }} + name: Comment Message + uses: actions/github-script@v8 + with: + script: | + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "[Preview] Preview has been removed." + }); + - if: ${{ steps.restore-pages.outputs.cache-matched-key }} + name: Remove History Cache + uses: actions/github-script@v8 + with: + script: | + github.rest.actions.deleteActionsCacheByKey({ + owner: context.repo.owner, + repo: context.repo.repo, + key: "${{ steps.restore-pages.outputs.cache-matched-key }}" + }); + - if: ${{ contains(github.event.pull_request.labels.*.name, 'preview/remove') || contains(github.event.issue.labels.*.name, 'preview/remove') }} + name: Remove Label (preview/remove) + continue-on-error: true + uses: actions/github-script@v8 + with: + script: | + github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: "preview/remove" + }); + - if: ${{ contains(github.event.pull_request.labels.*.name, 'preview/watch') || contains(github.event.issue.labels.*.name, 'preview/watch') }} + name: Remove Label (preview/watch) + continue-on-error: true + uses: actions/github-script@v8 + with: + script: | + github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: "preview/watch" + }); + preview-create-init: + if: >- + ${{ + !github.event.repository.fork && ( + ( github.event_name == 'pull_request_target' && ( + ( github.event.action == 'labeled' && contains(fromJSON('["preview/create", "preview/watch"]'), github.event.label.name) ) || + ( github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'preview/watch') ) + ) + ) || ( + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + contains(fromJSON('["/preview create", "/preview watch"]'), github.event.comment.body) && + contains('OWNER,MEMBER', github.event.comment.author_association) + ) + ) + }} + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + outputs: + sha: ${{ steps.init.outputs.sha }} + domain: ${{ steps.init.outputs.domain }} + pathname: ${{ steps.init.outputs.pathname }} + repository: ${{ steps.init.outputs.repository }} + check-id: ${{ steps.init.outputs.check-id }} + steps: + - id: init + name: Init + uses: actions/github-script@v8 + with: + script: | + const { owner, repo } = context.repo; + const pages = (await octokit.rest.repos.getPages({ owner, repo })).data; + const domain = pages.cname || `${owner.toLowerCase()}.github.io`; + core.setOutput('domain', domain); + core.setOutput('pathname', (pages.cname || repo.toLowerCase() === domain) ? '' : `/${repo}`); + const pr = context.payload.pull_request || (await github.rest.pulls.get({ + owner, repo, + pull_number: context.issue.number + })).data; + core.setOutput('sha', pr.head.sha); + core.setOutput('repository', pr.head.repo.full_name); + const { data: check } = await github.rest.checks.create({ + owner, repo, + name: "Create preview", + head_sha: pr.head.sha, + status: "in_progress", + started_at: new Date().toISOString(), + details_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${{ github.run_id }}`, + output: { + title: "Create preview by command", + summary: "Preview is being created..." + } + }); + core.setOutput('check-id', check.id); + - if: ${{ github.event_name == 'issue_comment' && github.event.comment.body == '/preview watch' }} + name: Add Label (preview/watch) + continue-on-error: true + uses: actions/github-script@v8 + with: + script: | + github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ["preview/watch"] + }); + preview-create-build: + needs: preview-create-init + runs-on: ubuntu-latest + outputs: + artifact-id: ${{ steps.upload-site.outputs.artifact-id }} + steps: + - name: Checkout PR + uses: actions/checkout@v5 + with: + ref: ${{ needs.preview-create-init.outputs.sha }} + repository: ${{ needs.preview-create-init.outputs.repository }} + fetch-depth: 1 + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.4" + bundler-cache: true + cache-version: pr${{ env.GITHUB_PR_NUMBER }} + - name: Jekyll Build + run: | + echo "url: https://${{ needs.preview-create-init.outputs.domain }}" > _action.yml + echo "baseurl: ${{ needs.preview-create-init.outputs.pathname }}/PR${{ env.GITHUB_PR_NUMBER }}" >> _action.yml + echo "source: ${{ github.workspace }}" >> _action.yml + echo "destination: /home/runner/site" >> _action.yml + bundle exec jekyll build --config _config.yml,_action.yml + - id: upload-site + name: Upload Site + uses: actions/upload-artifact@v4 + with: + name: site + path: /home/runner/site + preview-create-deploy: + needs: [preview-create-init, preview-create-build] + runs-on: ubuntu-latest + environment: + name: github-pages + url: https://${{ needs.preview-create-init.outputs.domain }}${{ needs.preview-create-init.outputs.pathname }}/PR${{ env.GITHUB_PR_NUMBER }} + permissions: + actions: write + pages: write + id-token: write + contents: write + steps: + - id: restore-pages + name: Restore Pages + uses: actions/cache/restore@v4 + with: + key: pages-${{ github.run_id }} + path: /home/runner/gh-pages + restore-keys: pages- + - name: Merge Site Before + run: | + mkdir -p /home/runner/gh-pages/data/{files,refs} + rm -rf /home/runner/gh-pages/PR${{ env.GITHUB_PR_NUMBER }} + grep -rlw 'PR${{ env.GITHUB_PR_NUMBER }}' /home/runner/gh-pages/data/refs | xargs -r sed -i '/\/d' + - name: Merge Site + uses: actions/download-artifact@v5 + with: + name: site + path: /home/runner/gh-pages/PR${{ env.GITHUB_PR_NUMBER }} + - name: Merge Site After + run: | + find /home/runner/gh-pages/PR${{ env.GITHUB_PR_NUMBER }} -type f | while read file; do + hash=$(sha256sum "$file" | cut -d' ' -f1) + size=$(stat -c%s "$file") + key="${hash}-${size}" + target="/home/runner/gh-pages/data/files/$key" + if [ ! -f "$target" ]; then + mv "$file" "$target" + else + rm -f "$file" + fi + ln -s "$(realpath --relative-to="$(dirname "$file")" "$target")" "$file" + echo "PR${{ env.GITHUB_PR_NUMBER }}" >> "/home/runner/gh-pages/data/refs/$key" + done + for ref in /home/runner/gh-pages/data/refs/*; do + if [ ! -s "$ref" ]; then + base=$(basename "$ref") + rm -f "/home/runner/gh-pages/data/files/$base" + rm -f "$ref" + fi + done + echo "[$(date)] Preview PR${{ env.GITHUB_PR_NUMBER }}" > /home/runner/gh-pages/index.html + - id: upload-pages + name: Upload Pages + uses: actions/upload-pages-artifact@v4 + with: + name: github-pages + path: /home/runner/gh-pages + - name: Deploy Pages + uses: actions/deploy-pages@v4 + with: + artifact_name: github-pages + - name: Save Pages + uses: actions/cache/save@v4 + with: + key: pages-${{ github.run_id }} + path: /home/runner/gh-pages + - if: ${{ steps.restore-pages.outputs.cache-matched-key }} + name: Remove History Cache + uses: actions/github-script@v8 + with: + script: | + github.rest.actions.deleteActionsCacheByKey({ + owner: context.repo.owner, + repo: context.repo.repo, + key: "${{ steps.restore-pages.outputs.cache-matched-key }}" + }); + - if: ${{ steps.upload-pages.outputs.artifact_id }} + name: Remove Pages Artifact + uses: actions/github-script@v8 + with: + script: | + github.rest.actions.deleteArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: "${{ steps.upload-pages.outputs.artifact_id }}", + }); + - if: ${{ needs.preview-create-build.outputs.artifact-id }} + name: Remove Temp Artifact + uses: actions/github-script@v8 + with: + script: | + github.rest.actions.deleteArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: "${{ needs.preview-create-build.outputs.artifact-id }}" + }); + preview-create-finally: + if: ${{ always() && needs.preview-create-init.result == 'success' }} + needs: [preview-create-init, preview-create-deploy] + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + steps: + - if: ${{ github.event_name == 'issue_comment' }} + id: comment-message + name: Comment Message (issue_comment) + env: + PREVIEW_URL: https://${{ needs.preview-create-init.outputs.domain }}${{ needs.preview-create-init.outputs.pathname }}/PR${{ env.GITHUB_PR_NUMBER }} + continue-on-error: true + uses: actions/github-script@v8 + with: + script: | + github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + issue_number: context.issue.number, + body: "${{ needs.preview-create-deploy.result == 'success' && format('[Preview] {0}', env.PREVIEW_URL) || '[Preview] Failed, see logs for more information.' }}" + }); + - if: ${{ steps.comment-message.outcome == 'failure' }} + name: Comment Message + env: + PREVIEW_URL: https://${{ needs.preview-create-init.outputs.domain }}${{ needs.preview-create-init.outputs.pathname }}/PR${{ env.GITHUB_PR_NUMBER }} + uses: actions/github-script@v8 + with: + script: | + github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "${{ needs.preview-create-deploy.result == 'success' && format('[Preview] {0}', env.PREVIEW_URL) || '[Preview] Failed, see logs for more information.' }}" + }); + - if: ${{ contains(github.event.pull_request.labels.*.name, 'preview/create') || contains(github.event.issue.labels.*.name, 'preview/create') }} + name: Remove Label (preview/create) + continue-on-error: true + uses: actions/github-script@v8 + with: + script: | + github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: "preview/create" + }); + - name: Check Over + env: + PREVIEW_URL: https://${{ needs.preview-create-init.outputs.domain }}${{ needs.preview-create-init.outputs.pathname }}/PR${{ env.GITHUB_PR_NUMBER }} + uses: actions/github-script@v8 + with: + script: | + github.rest.checks.update({ + owner: context.repo.owner, + repo: context.repo.repo, + check_run_id: ${{ needs.preview-create-init.outputs.check-id }}, + status: "completed", + conclusion: "${{ needs.preview-create-deploy.result == 'success' && 'success' || 'failure' }}", + completed_at: new Date().toISOString(), + details_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/runs/${{ github.run_id }}`, + output: { + title: "Create preview ${{ needs.preview-create-deploy.result == 'success' && 'success' || 'failure' }}", + summary: "${{ needs.preview-create-deploy.result == 'success' && format('[Preview] {0}', env.PREVIEW_URL) || '[Preview] Failed, see logs for more information.' }}" + } + });