diff --git a/.github/workflows/pr-docs-preview-publish.yml b/.github/workflows/pr-docs-preview-publish.yml new file mode 100644 index 000000000..2e7597e29 --- /dev/null +++ b/.github/workflows/pr-docs-preview-publish.yml @@ -0,0 +1,118 @@ +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: + workflows: ["Create docs preview on PR"] + types: [completed] + +permissions: + contents: write + pull-requests: write + actions: read + +# One group per PR branch: a newer run replaces a pending older run. +concurrency: + group: docs-preview-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} + 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. + # Remove the preview when the PR is not open. + - 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; } + 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" + + - 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 --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 + [[ -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="" + 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} + 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..4928aa172 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.pull_request.state == 'open' 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.pull_request.state == 'open' uses: actions/setup-python@v7 with: python-version: "3.12" - name: Cache pip dependencies + if: github.event.pull_request.state == 'open' uses: actions/cache@v6 with: path: ~/.cache/pip @@ -42,6 +38,7 @@ jobs: ${{ runner.os }}-pip- - name: Install dependencies + if: github.event.pull_request.state == 'open' 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.pull_request.state == 'open' 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 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.pull_request.state == 'open' && 'publish' || 'remove' }} 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