From 21896510451f0540696976b5391989dde5e864dd Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Fri, 30 Oct 2026 02:52:25 +0100 Subject: [PATCH 1/2] ci: publish docs previews for fork PRs A PR from a fork gets a read-only token, so its preview was never published. Build with the read-only token and upload the site as an artifact. A workflow_run workflow publishes it to www and comments. Signed-off-by: Igor Pecovnik --- .github/workflows/pr-docs-preview-publish.yml | 107 ++++++++++++++++++ .github/workflows/pr-docs-preview.yml | 100 ++++------------ 2 files changed, 131 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/pr-docs-preview-publish.yml diff --git a/.github/workflows/pr-docs-preview-publish.yml b/.github/workflows/pr-docs-preview-publish.yml new file mode 100644 index 000000000..0fd4366ca --- /dev/null +++ b/.github/workflows/pr-docs-preview-publish.yml @@ -0,0 +1,107 @@ +name: Publish docs preview on PR + +# Publish what pr-docs-preview.yml built, and remove it when the PR closes. +# This workflow has a write token, so it never runs code from the PR: +# it only copies files and treats the artifact as untrusted data. + +on: + workflow_run: + workflows: ["Create docs preview on PR"] + types: [completed] + +permissions: + contents: write + pull-requests: write + actions: read + +concurrency: + group: docs-preview-www + cancel-in-progress: false + +jobs: + publish: + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + + steps: + - name: Download preview + uses: actions/download-artifact@v8 + with: + name: docs-preview + path: preview + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + # Accept only a PR whose head is the commit that was built. + - name: Check preview metadata + id: meta + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + set -euo pipefail + PR=$(head -c 16 preview/pr | tr -d '\n') + ACTION=$(head -c 16 preview/action | tr -d '\n') + [[ "${PR}" =~ ^[0-9]+$ ]] || { echo "::error::Invalid PR number"; exit 1; } + [[ "${ACTION}" =~ ^(publish|remove)$ ]] || { echo "::error::Invalid action"; exit 1; } + PR_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}" --jq .head.sha) + if [[ "${PR_SHA}" != "${HEAD_SHA}" ]]; then + # A newer push or a wrong PR number. A newer run publishes instead. + echo "::notice::PR #${PR} head is not ${HEAD_SHA}. Skip." + ACTION=skip + fi + echo "pr=${PR}" >> "$GITHUB_OUTPUT" + echo "action=${ACTION}" >> "$GITHUB_OUTPUT" + + - name: Checkout `www` branch + if: steps.meta.outputs.action != 'skip' + uses: actions/checkout@v7 + with: + ref: www + path: www + + # Remove previews of closed PRs, so www stays below the 1 GB Pages limit. + - name: Update `www` branch + if: steps.meta.outputs.action != 'skip' + working-directory: www + env: + PR: ${{ steps.meta.outputs.pr }} + ACTION: ${{ steps.meta.outputs.action }} + run: | + set -euo pipefail + git config user.name github-actions + git config user.email github-actions@github.com + if [[ "${ACTION}" == "publish" ]]; then + mkdir -p "${PR}" + rsync -a --delete --no-links ../preview/site/ "${PR}/" + git add -A "${PR}" + git commit -q -m "Update docs preview for PR #${PR}" || { echo "No changes."; exit 0; } + else + [[ -d "${PR}" ]] || { echo "No preview for PR #${PR}."; exit 0; } + git rm -r -q "${PR}" + git commit -q -m "www: remove docs preview for closed PR #${PR}" + fi + for attempt in 1 2 3; do + git push origin www && exit 0 + echo "Push failed (attempt ${attempt}). Rebase on the latest www." >&2 + git pull -q --rebase origin www + done + echo "::error::Push to www failed after 3 attempts" + exit 1 + + # One comment per PR. Later runs leave it as it is. + - name: Comment on PR with preview link + if: steps.meta.outputs.action == 'publish' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR: ${{ steps.meta.outputs.pr }} + run: | + set -euo pipefail + MARKER="" + if gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate --jq '.[].body' | grep -qF "${MARKER}"; then + exit 0 + fi + gh pr comment "${PR}" -R "${GITHUB_REPOSITORY}" --body "${MARKER} + Documentation preview, available a few minutes after each push: + +
Open WWW preview
" diff --git a/.github/workflows/pr-docs-preview.yml b/.github/workflows/pr-docs-preview.yml index 9b9433c7f..b9417c5a6 100644 --- a/.github/workflows/pr-docs-preview.yml +++ b/.github/workflows/pr-docs-preview.yml @@ -1,39 +1,35 @@ name: Create docs preview on PR +# Build only, with a read-only token, so fork PRs work too. +# pr-docs-preview-publish.yml publishes the result and comments on the PR. + on: pull_request: types: [opened, reopened, synchronize, labeled, closed] - workflow_dispatch: permissions: - contents: write - pull-requests: write + contents: read jobs: docs-preview: - # Build/publish a preview only while the PR is active; on close we clean up instead. - if: github.event.action != 'closed' runs-on: ubuntu-latest steps: - name: Checkout PR branch (documentation) + if: github.event.action != 'closed' uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} path: documentation - - name: Checkout `www` branch - uses: actions/checkout@v7 - with: - ref: www - path: www - - - name: Set up Python 3.8 + - name: Set up Python + if: github.event.action != 'closed' uses: actions/setup-python@v7 with: python-version: "3.12" - name: Cache pip dependencies + if: github.event.action != 'closed' uses: actions/cache@v6 with: path: ~/.cache/pip @@ -42,6 +38,7 @@ jobs: ${{ runner.os }}-pip- - name: Install dependencies + if: github.event.action != 'closed' working-directory: documentation run: | python -m pip install --upgrade pip @@ -49,72 +46,23 @@ jobs: pip install -r requirements.txt - name: Build docs with MkDocs + if: github.event.action != 'closed' working-directory: documentation - run: mkdocs build --clean - - # Publishing the preview and commenting need a writable GITHUB_TOKEN, - # which pull_request from a fork does not get (token is read-only there). - # Skip these steps for fork PRs so the check stays green after the build - # step has already validated the docs; internal branches still publish. - - name: Publish to `www` branch - if: github.event.pull_request.head.repo.full_name == github.repository - working-directory: www - run: | - mkdir -p "${{ github.event.number }}" - rsync -av ../documentation/site/. "${{ github.event.number }}" - git config user.name github-actions - git config user.email github-actions@github.com - git add . - git commit -m "Update docs preview for PR #${{ github.event.number }}" || true - git push origin www || true - - - name: Comment on PR with preview link - if: github.event.pull_request.head.repo.full_name == github.repository - uses: devindford/Append_PR_Comment@32dd2619cd96ac8da9907c416c992fe265233ca8 # v1.1.3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - body-template: | - [![Create docs preview on PR](https://github.com/armbian/documentation/actions/workflows/pr-docs-preview.yml/badge.svg)](https://github.com/armbian/documentation/actions/workflows/pr-docs-preview.yml) - - Documentation website preview will be available shortly: + run: mkdocs build --clean --site-dir ../preview/site -
 Open WWW preview 
- body-update-action: suffix - - # When a PR is closed (merged or not), delete its preview build from the `www` - # branch so previews stop accumulating there. Without this, every PR left a - # full site copy under www// forever, which grew the GitHub Pages source - # past its 1 GB limit and broke deployment. - cleanup-preview: - if: github.event.action == 'closed' && github.event.pull_request.head.repo.full_name == github.repository - runs-on: ubuntu-latest - steps: - - name: Checkout `www` branch - uses: actions/checkout@v7 - with: - ref: www - - - name: Remove preview for closed PR + # On close, only the metadata goes up, so the publish workflow removes the preview. + - name: Write preview metadata env: PR: ${{ github.event.number }} + ACTION: ${{ github.event.action == 'closed' && 'remove' || 'publish' }} run: | - set -euo pipefail - if [ ! -d "${PR}" ]; then - echo "No preview directory for PR #${PR}; nothing to do." - exit 0 - fi - git config user.name github-actions - git config user.email github-actions@github.com - git rm -r --quiet "${PR}" - git commit -m "www: remove docs preview for closed PR #${PR}" - # Retry against concurrent preview pushes from other PRs. - for attempt in 1 2 3; do - if git push origin www; then - echo "Removed preview for PR #${PR}." - exit 0 - fi - echo "Push failed (attempt ${attempt}); rebasing on latest www..." >&2 - git pull --rebase origin www - done - echo "Failed to push preview removal for PR #${PR} after 3 attempts." >&2 - exit 1 \ No newline at end of file + mkdir -p preview + echo "${PR}" > preview/pr + echo "${ACTION}" > preview/action + + - name: Upload preview + uses: actions/upload-artifact@v7 + with: + name: docs-preview + path: preview/ + retention-days: 1 From 9cda54a9ec9db27a5c993fc76daaf50085a33946 Mon Sep 17 00:00:00 2001 From: Igor Pecovnik Date: Fri, 30 Oct 2026 03:02:04 +0100 Subject: [PATCH 2/2] ci: docs preview review fixes Publish only for trusted PR authors and for open PRs. Use one concurrency group per PR branch. Fix the duplicate comment check. Signed-off-by: Igor Pecovnik --- .github/workflows/pr-docs-preview-publish.yml | 19 +++++++++++++++---- .github/workflows/pr-docs-preview.yml | 14 +++++++------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-docs-preview-publish.yml b/.github/workflows/pr-docs-preview-publish.yml index 0fd4366ca..2e7597e29 100644 --- a/.github/workflows/pr-docs-preview-publish.yml +++ b/.github/workflows/pr-docs-preview-publish.yml @@ -3,6 +3,8 @@ name: Publish docs preview on PR # Publish what pr-docs-preview.yml built, and remove it when the PR closes. # This workflow has a write token, so it never runs code from the PR: # it only copies files and treats the artifact as untrusted data. +# The author of a fork PR controls the artifact, including any HTML and +# JavaScript. So publish only for PR authors with write or member access. on: workflow_run: @@ -14,8 +16,9 @@ permissions: pull-requests: write actions: read +# One group per PR branch: a newer run replaces a pending older run. concurrency: - group: docs-preview-www + group: docs-preview-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} cancel-in-progress: false jobs: @@ -33,6 +36,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} # Accept only a PR whose head is the commit that was built. + # Remove the preview when the PR is not open. - name: Check preview metadata id: meta env: @@ -44,11 +48,17 @@ jobs: ACTION=$(head -c 16 preview/action | tr -d '\n') [[ "${PR}" =~ ^[0-9]+$ ]] || { echo "::error::Invalid PR number"; exit 1; } [[ "${ACTION}" =~ ^(publish|remove)$ ]] || { echo "::error::Invalid action"; exit 1; } - PR_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}" --jq .head.sha) + read -r PR_SHA PR_STATE PR_ASSOC < <(gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR}" \ + --jq '"\(.head.sha) \(.state) \(.author_association)"') if [[ "${PR_SHA}" != "${HEAD_SHA}" ]]; then # A newer push or a wrong PR number. A newer run publishes instead. echo "::notice::PR #${PR} head is not ${HEAD_SHA}. Skip." ACTION=skip + elif [[ "${PR_STATE}" != "open" ]]; then + ACTION=remove + elif [[ "${ACTION}" == "publish" && ! "${PR_ASSOC}" =~ ^(OWNER|MEMBER|COLLABORATOR)$ ]]; then + echo "::notice::PR #${PR} author is ${PR_ASSOC}. No preview." + ACTION=skip fi echo "pr=${PR}" >> "$GITHUB_OUTPUT" echo "action=${ACTION}" >> "$GITHUB_OUTPUT" @@ -73,7 +83,7 @@ jobs: git config user.email github-actions@github.com if [[ "${ACTION}" == "publish" ]]; then mkdir -p "${PR}" - rsync -a --delete --no-links ../preview/site/ "${PR}/" + rsync -a --delete --no-links --exclude='.git*' ../preview/site/ "${PR}/" git add -A "${PR}" git commit -q -m "Update docs preview for PR #${PR}" || { echo "No changes."; exit 0; } else @@ -98,7 +108,8 @@ jobs: run: | set -euo pipefail MARKER="" - if gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate --jq '.[].body' | grep -qF "${MARKER}"; then + COMMENTS=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" --paginate --jq '.[].body') + if grep -qF "${MARKER}" <<< "${COMMENTS}"; then exit 0 fi gh pr comment "${PR}" -R "${GITHUB_REPOSITORY}" --body "${MARKER} diff --git a/.github/workflows/pr-docs-preview.yml b/.github/workflows/pr-docs-preview.yml index b9417c5a6..4928aa172 100644 --- a/.github/workflows/pr-docs-preview.yml +++ b/.github/workflows/pr-docs-preview.yml @@ -16,20 +16,20 @@ jobs: steps: - name: Checkout PR branch (documentation) - if: github.event.action != 'closed' + if: github.event.pull_request.state == 'open' uses: actions/checkout@v7 with: ref: ${{ github.event.pull_request.head.sha }} path: documentation - name: Set up Python - if: github.event.action != 'closed' + if: github.event.pull_request.state == 'open' uses: actions/setup-python@v7 with: python-version: "3.12" - name: Cache pip dependencies - if: github.event.action != 'closed' + if: github.event.pull_request.state == 'open' uses: actions/cache@v6 with: path: ~/.cache/pip @@ -38,7 +38,7 @@ jobs: ${{ runner.os }}-pip- - name: Install dependencies - if: github.event.action != 'closed' + if: github.event.pull_request.state == 'open' working-directory: documentation run: | python -m pip install --upgrade pip @@ -46,15 +46,15 @@ jobs: pip install -r requirements.txt - name: Build docs with MkDocs - if: github.event.action != 'closed' + if: github.event.pull_request.state == 'open' working-directory: documentation run: mkdocs build --clean --site-dir ../preview/site - # On close, only the metadata goes up, so the publish workflow removes the preview. + # On a closed PR, only the metadata goes up, so the publish workflow removes the preview. - name: Write preview metadata env: PR: ${{ github.event.number }} - ACTION: ${{ github.event.action == 'closed' && 'remove' || 'publish' }} + ACTION: ${{ github.event.pull_request.state == 'open' && 'publish' || 'remove' }} run: | mkdir -p preview echo "${PR}" > preview/pr