From 0ae625c335b5d2e561ef447ecb76a868fab00f4e Mon Sep 17 00:00:00 2001 From: Arkadiusz Balys Date: Wed, 28 Jan 2026 10:20:18 +0100 Subject: [PATCH 1/4] ci: Clean unused documentation preview Added a functionality to take care of obsolete PR-previews removal to ensure that in the new PRs, there are no redundant ones. Since we have this mechanism, we do not need post-pr clean workflow because all the obsolete workflows will be deleted once a new PR is created or after 7 days. Signed-off-by: Arkadiusz Balys --- .github/actions/cleanup-site/action.yml | 130 ++++++++++++++++++++++++ .github/workflows/docpreview.yml | 45 ++++---- .github/workflows/docremove.yml | 123 ---------------------- .github/workflows/post_pr.yml | 26 ----- 4 files changed, 158 insertions(+), 166 deletions(-) create mode 100644 .github/actions/cleanup-site/action.yml delete mode 100644 .github/workflows/docremove.yml delete mode 100644 .github/workflows/post_pr.yml diff --git a/.github/actions/cleanup-site/action.yml b/.github/actions/cleanup-site/action.yml new file mode 100644 index 00000000..ad93e5c3 --- /dev/null +++ b/.github/actions/cleanup-site/action.yml @@ -0,0 +1,130 @@ +# Cleanup site state: remove pr-preview directories for closed PRs. +# Keeps only directories for currently open PRs so saved state matches reality. +# This action keeps the existing pages only for the actually opened PRs to avoid restoring +# the pages of closed PRs. + +name: 'Cleanup site state' +description: 'Remove PR preview directories for closed PRs; keep only open PRs in site state' + +inputs: + site_dir: + description: 'Path to the site directory (e.g. _site) containing pr-preview subdirs' + required: true + +runs: + using: 'composite' + steps: + - name: Collect list of open PRs + id: open-prs + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + + # Build JSON array of open PR numbers for use in next step. + if ! OPEN_PRS_JSON=$(gh pr list --state open --json number -q '.[].number' --limit 100 | jq -R -s -c '[split("\n")[] | select(length > 0) | tonumber]'); then + echo "Error: Failed to list open PRs" >&2 + exit 1 + fi + + # Validate the output is valid JSON array + if ! echo "$OPEN_PRS_JSON" | jq -e 'type == "array"' >/dev/null 2>&1; then + echo "Error: Invalid JSON output from PR list" >&2 + exit 1 + fi + + echo "open_prs_json=$OPEN_PRS_JSON" >> "$GITHUB_OUTPUT" + echo "Found $(echo "$OPEN_PRS_JSON" | jq 'length') open PR(s)" + + - name: Remove closed PR preview directories + shell: bash + env: + SITE_DIR: ${{ inputs.site_dir }} + OPEN_PRS_JSON: ${{ steps.open-prs.outputs.open_prs_json }} + run: | + set -e + + # Validate site_dir input + if [ -z "$SITE_DIR" ]; then + echo "Error: site_dir is empty" >&2 + exit 1 + fi + + # Reject paths with path traversal, absolute paths, or control characters + if [[ "$SITE_DIR" == *".."* ]] || \ + [[ "$SITE_DIR" == /* ]] || \ + [[ "$SITE_DIR" == *$'\n'* ]] || \ + [[ "$SITE_DIR" == *$'\r'* ]] || \ + [[ "$SITE_DIR" == *$'\t'* ]]; then + echo "Error: Invalid site_dir (path traversal or absolute path)" >&2 + exit 1 + fi + + # Get absolute path of workspace root for boundary checking + WORKSPACE_ROOT=$(cd "$GITHUB_WORKSPACE" && pwd) + + # Resolve and validate site_dir path + if [ ! -d "$SITE_DIR" ]; then + echo "Info: site_dir does not exist: $SITE_DIR (first run or no previous state)" >&2 + exit 0 + fi + + RESOLVED_SITE_DIR=$(cd "$SITE_DIR" && pwd) + + # Ensure resolved path is within workspace + if [[ "$RESOLVED_SITE_DIR" != "$WORKSPACE_ROOT"* ]]; then + echo "Error: site_dir resolves outside workspace" >&2 + exit 1 + fi + + PR_PREVIEW="${RESOLVED_SITE_DIR}/pr-preview" + + # Validate PR_PREVIEW path is still within workspace + if [[ "$PR_PREVIEW" != "$WORKSPACE_ROOT"* ]]; then + echo "Error: PR_PREVIEW path escapes workspace" >&2 + exit 1 + fi + + # If there is no previews, just exit + if [ ! -d "$PR_PREVIEW" ]; then + exit 0 + fi + + # Process each pr-* directory + for dir in "$PR_PREVIEW"/pr-*; do + RESOLVED_DIR=$(cd "$dir" && pwd) + + # Skip if glob didn't match anything, files, symlinks, etc. + if [ ! -e "$dir" ] || [ ! -d "$dir" ] || [[ "$RESOLVED_DIR" != "$WORKSPACE_ROOT"* ]] || [[ "$RESOLVED_DIR" != "$PR_PREVIEW"* ]]; then + continue + fi + name=$(basename "$dir") + + # Validate directory name matches expected pattern (pr- followed by digits only) + if [[ "$name" =~ ^pr-([0-9]+)$ ]]; then + pr_num="${BASH_REMATCH[1]}" + + # Validate pr_num is numeric (extra safety) + if ! [[ "$pr_num" =~ ^[0-9]+$ ]]; then + echo "Warning: Invalid PR number extracted: $pr_num" >&2 + continue + fi + + # Validate OPEN_PRS_JSON is valid before using it + if ! echo "$OPEN_PRS_JSON" | jq -e 'type == "array"' >/dev/null 2>&1; then + echo "Error: Invalid OPEN_PRS_JSON format" >&2 + exit 1 + fi + + # Check if PR is still open + if ! echo "$OPEN_PRS_JSON" | jq -e --argjson n "$pr_num" 'index($n) != null' >/dev/null 2>&1; then + rm -rf "$dir" + fi + fi + done + + # Clean up empty pr-preview directory + if [ -d "$PR_PREVIEW" ] && [ -z "$(ls -A "$PR_PREVIEW" 2>/dev/null)" ]; then + rm -rf "$PR_PREVIEW" + fi diff --git a/.github/workflows/docpreview.yml b/.github/workflows/docpreview.yml index 61095d35..eee14caf 100644 --- a/.github/workflows/docpreview.yml +++ b/.github/workflows/docpreview.yml @@ -30,40 +30,51 @@ jobs: - name: Checkout repository uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4 - - name: Download site state from cleanup - id: download-cleanup - uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6 - with: - workflow: docremove.yml - name: site-state - path: _existing_pages - workflow_conclusion: success - search_artifacts: true - if_no_artifact_found: warn - continue-on-error: true - - name: Download site state from deploy id: download-deploy - if: steps.download-cleanup.outcome == 'failure' uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6 with: workflow: docpreview.yml name: site-state - path: _existing_pages + path: ${{ runner.temp }}/existing_pages workflow_conclusion: success search_artifacts: true if_no_artifact_found: warn - continue-on-error: true + continue-on-error: false - name: Remove symlinks from downloaded artifacts run: | - find _existing_pages -type l -delete 2>/dev/null || true + if [ -d "${{ runner.temp }}/existing_pages" ]; then + find "${{ runner.temp }}/existing_pages" -type l -delete 2>/dev/null || true + rm -rf _existing_pages + mkdir -p _existing_pages + cp -a "${{ runner.temp }}/existing_pages/." _existing_pages/ || true + fi - name: Download build artifacts uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6 with: workflow: docbuild.yml run_id: ${{ github.event.workflow_run.id }} + path: ${{ runner.temp }} + + - name: Move doc-preview from temp + run: | + if [ -d "doc-preview" ]; then + rm -rf doc-preview + fi + if [ -d "${{ runner.temp }}/doc-preview" ]; then + mv "${{ runner.temp }}/doc-preview" ./doc-preview + else + echo "Expected doc-preview directory not found in runner.temp" >&2 + exit 1 + fi + + - name: Cleanup site state + id: download-site-state + uses: ./.github/actions/cleanup-site + with: + site_dir: _existing_pages - name: Extract version and unzip docs working-directory: doc-preview @@ -123,7 +134,7 @@ jobs: with: name: site-state path: _site - retention-days: 14 # 2 weeks + retention-days: 30 # 1 month, recall Build Documentation workflow to re-generate preview overwrite: true - name: Find existing comment diff --git a/.github/workflows/docremove.yml b/.github/workflows/docremove.yml deleted file mode 100644 index 3ae9db7f..00000000 --- a/.github/workflows/docremove.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: Cleanup PR preview - -on: - workflow_run: - workflows: ["PR closed"] - types: - - completed - -permissions: - contents: read - pages: write - id-token: write - actions: write - -# Use same concurrency group as deploy to avoid conflicts -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - cleanup: - runs-on: ubuntu-24.04 - if: ${{ github.event.workflow_run.conclusion == 'success' }} - environment: - name: github-pages - - steps: - - name: Download PR info artifact - uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6 - with: - workflow: post_pr.yml - run_id: ${{ github.event.workflow_run.id }} - name: pr-closed-info - - - name: Read PR info - id: pr-info - run: | - PR_NUMBER=$(cat pr_number.txt | tr -cd '0-9') - echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT - echo "PR number: $PR_NUMBER" - - - name: Download site state from deploy - id: download-deploy - uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6 - with: - workflow: docpreview.yml - name: site-state - path: _site - workflow_conclusion: success - search_artifacts: true - if_no_artifact_found: warn - continue-on-error: true - - - name: Download site state from cleanup - id: download-cleanup - if: steps.download-deploy.outcome == 'failure' - uses: dawidd6/action-download-artifact@bf251b5aa9c2f7eeb574a96ee720e24f801b7c11 # v6 - with: - workflow: docremove.yml - name: site-state - path: _site - workflow_conclusion: success - search_artifacts: true - if_no_artifact_found: warn - continue-on-error: true - - - name: Verify site state exists - run: | - if [ ! -d "_site" ] || [ -z "$(ls -A _site 2>/dev/null)" ]; then - echo "Error: Could not download site-state artifact from any workflow" - echo "This may happen if no documentation has been deployed yet" - exit 1 - fi - # Remove symlinks from downloaded artifacts (security) - find _site -type l -delete - echo "Site state downloaded successfully" - ls -la _site/ - - - name: Remove PR preview directory - id: cleanup - run: | - pr_number="${{ steps.pr-info.outputs.pr_number }}" - preview_dir="_site/pr-preview/pr-${pr_number}" - - if [ -d "$preview_dir" ]; then - echo "Removing preview directory: $preview_dir" - rm -rf "$preview_dir" - - # Check if pr-preview directory is empty and remove if so - if [ -d "_site/pr-preview" ] && [ -z "$(ls -A _site/pr-preview)" ]; then - rm -rf "_site/pr-preview" - fi - - echo "cleaned_up=true" >> $GITHUB_OUTPUT - echo "Preview cleaned up successfully" - else - echo "No preview directory found for PR #${pr_number}" - echo "cleaned_up=false" >> $GITHUB_OUTPUT - fi - - - name: Setup Pages - if: steps.cleanup.outputs.cleaned_up == 'true' - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 - - - name: Upload Pages artifact - if: steps.cleanup.outputs.cleaned_up == 'true' - uses: actions/upload-pages-artifact@0252fc4ba7626f0298f0cf00902a25c6afc77fa8 # v3 - with: - path: '_site' - - - name: Deploy to GitHub Pages - if: steps.cleanup.outputs.cleaned_up == 'true' - uses: actions/deploy-pages@f33f41b675f0ab2dc5a6863c9a170fe83af3571e # v4 - - # Save updated site state - - name: Save site state artifact - if: steps.cleanup.outputs.cleaned_up == 'true' - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 - with: - name: site-state - path: _site - retention-days: 14 # 2 weeks - overwrite: true diff --git a/.github/workflows/post_pr.yml b/.github/workflows/post_pr.yml deleted file mode 100644 index 126e9ca4..00000000 --- a/.github/workflows/post_pr.yml +++ /dev/null @@ -1,26 +0,0 @@ - -name: PR closed - -on: - pull_request: - types: [closed] - -permissions: - contents: read - -jobs: - save_pr_info: - runs-on: ubuntu-24.04 - - steps: - - name: Save PR info for cleanup - run: | - echo "${{ github.event.pull_request.number }}" > pr_number.txt - - - name: Upload PR info artifact - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4 - with: - name: pr-closed-info - path: | - pr_number.txt - retention-days: 1 From 8112b327486998a58b309b4ae82b3742a8cfd46f Mon Sep 17 00:00:00 2001 From: Arkadiusz Balys Date: Wed, 28 Jan 2026 10:45:28 +0100 Subject: [PATCH 2/4] ci: Simplify doc preview note The doc preview note seems to be too long. Try to simplify it. Signed-off-by: Arkadiusz Balys --- .../actions/generate-docs-comment/action.yml | 77 +++++++++---------- 1 file changed, 38 insertions(+), 39 deletions(-) diff --git a/.github/actions/generate-docs-comment/action.yml b/.github/actions/generate-docs-comment/action.yml index d66d037b..41b7b770 100644 --- a/.github/actions/generate-docs-comment/action.yml +++ b/.github/actions/generate-docs-comment/action.yml @@ -40,58 +40,57 @@ runs: build_timestamp="$BUILD_TIMESTAMP" commit_sha="$COMMIT_SHA" - cat << 'EOF' > comment.md - ## Documentation Preview - - The documentation has been built successfully and is available for preview. - - | Build Info | Value | - |------------|-------| - | **Generated at** | `$TIMESTAMP_PLACEHOLDER` | - | **Commit** | `$COMMIT_PLACEHOLDER` | - - ### Preview Links - - | Page | Preview Link | - |------|--------------| - | **Index (Home)** | [View Preview]($preview_url_placeholder/index.html) | - EOF - - # Replace placeholders with actual values - sed -i "s|\$preview_url_placeholder|$preview_url|g" comment.md - sed -i "s|\$TIMESTAMP_PLACEHOLDER|$build_timestamp|g" comment.md - sed -i "s|\$COMMIT_PLACEHOLDER|$commit_sha|g" comment.md + # Initialize comment.md file + echo "## Documentation Preview" > comment.md + echo "" >> comment.md + echo "The documentation has been built successfully. You can view the preview here: [preview]($preview_url/index.html)" >> comment.md + echo "" >> comment.md + echo "**Generated at**: \`$build_timestamp\` with commit \`$commit_sha\`." >> comment.md # Add links to changed files - if [ "$changed_docs" != "[]" ] && [ -n "$changed_docs" ]; then + # Parse JSON and get count of items + doc_count=$(echo "$changed_docs" | jq -r 'if type == "array" then length else 0 end' 2>/dev/null || echo "0") + + # Always show details section to ensure GitHub renders it + # Check if we have any files to show + has_files=false + if [ "$doc_count" != "0" ] && [ "$doc_count" != "null" ] && [ -n "$doc_count" ]; then + # Verify it's actually a number and > 0 + if [ "$doc_count" -gt 0 ] 2>/dev/null; then + has_files=true + fi + fi + + echo "" >> comment.md + echo "
" >> comment.md + echo "Expand to view changed pages" >> comment.md + echo "" >> comment.md + + if [ "$has_files" = "true" ]; then echo "" >> comment.md - echo "### Changed Pages" >> comment.md echo "" >> comment.md - echo "| Source File | Preview Link |" >> comment.md - echo "|-------------|--------------|" >> comment.md - - echo "$changed_docs" | jq -r '.[]' | while read -r file; do - if [ -n "$file" ]; then - # Convert .rst/.md path to .html path - # Remove 'docs/' prefix and change extension + echo "| File | Preview |" >> comment.md + echo "| ---- | ------- |" >> comment.md + while IFS= read -r file; do + if [ -n "$file" ] && [ "$file" != "null" ]; then html_path=$(echo "$file" | sed 's|^docs/||' | sed 's|\.rst$|.html|' | sed 's|\.md$|.html|') - # Handle index files in subdirectories if [[ "$html_path" == */index.html ]]; then html_path="${html_path}" fi - filename=$(basename "$file") echo "| \`$file\` | [View]($preview_url/$html_path) |" >> comment.md fi - done + done < <(echo "$changed_docs" | jq -r '.[]' 2>/dev/null) + else + echo "No documentation files were changed in this PR." >> comment.md fi - cat << 'EOF' >> comment.md - - --- + echo "" >> comment.md + echo "
" >> comment.md - > **Note:** The preview will be available after GitHub Pages deployment completes (usually 1-2 minutes). - > If you see stale content, hard-refresh your browser (Ctrl+Shift+R) or wait a moment for GitHub Pages to update. - EOF + echo "" >> comment.md + echo "---" >> comment.md + echo "" >> comment.md + echo "> **Note:** The preview will be available after GitHub Pages deployment completes (usually 1-2 minutes). Ensure that **generated at** timestamp is up to date. If the Pull Request is stale for more than 30 days, the preview may be deleted. In this case, please update the Pull Request, or re-run the Build Documentation workflow to generate a new preview." >> comment.md echo "Comment content:" cat comment.md From bfc964faf32c5a030331c56762e36ad12c4daff7 Mon Sep 17 00:00:00 2001 From: Arkadiusz Balys Date: Wed, 28 Jan 2026 10:54:57 +0100 Subject: [PATCH 3/4] ci: Enable running doc-preview workflow on changes in samples Tigger doc-build workflows on changes in README.rst files in samples. Signed-off-by: Arkadiusz Balys --- .github/actions/get-changed-docs/action.yml | 2 +- .github/workflows/docbuild.yml | 3 ++- docs/requirements-doc.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/actions/get-changed-docs/action.yml b/.github/actions/get-changed-docs/action.yml index c638d91a..b68bce9b 100644 --- a/.github/actions/get-changed-docs/action.yml +++ b/.github/actions/get-changed-docs/action.yml @@ -40,7 +40,7 @@ runs: HEAD_SHA: ${{ inputs.head_sha }} run: | # Get list of changed .rst and .md files in docs/ - changed_files=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'docs/*.rst' 'docs/*.md' 'docs/**/*.rst' 'docs/**/*.md' | head -20) + changed_files=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'docs/*.rst' 'docs/*.md' 'docs/**/*.rst' 'docs/**/*.md' 'samples/**/*.rst' | head -20) echo "Changed documentation files:" echo "$changed_files" diff --git a/.github/workflows/docbuild.yml b/.github/workflows/docbuild.yml index 3a9dee25..275afa4b 100644 --- a/.github/workflows/docbuild.yml +++ b/.github/workflows/docbuild.yml @@ -6,7 +6,8 @@ on: - main paths: - 'docs/**' - - '.github/workflows/docbuild.yml' + - '.github/workflows/doc*' + - 'samples/**/*.rst' permissions: contents: read diff --git a/docs/requirements-doc.txt b/docs/requirements-doc.txt index 8c2b371d..3d893a04 100644 --- a/docs/requirements-doc.txt +++ b/docs/requirements-doc.txt @@ -26,7 +26,7 @@ sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.5 # Jinja2 compatibility for Sphinx 4.x -Jinja2<3.1 +Jinja2>=3.1.6 MarkupSafe>=2.0.0,<2.2.0 # Other dependencies From b1a5b9e9aaa43514efc28ae22d7927a02db669b3 Mon Sep 17 00:00:00 2001 From: Arkadiusz Balys Date: Fri, 30 Jan 2026 07:43:53 +0100 Subject: [PATCH 4/4] ci: Do no call on_pr workflow on changes in doc-preview. The on_pr workflow only builds samples using Twister. If there is a change in doc-preview-related files, there is no need to run it. Signed-off-by: Arkadiusz Balys --- .github/workflows/on_pr.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/on_pr.yml b/.github/workflows/on_pr.yml index 4f65e3eb..1de5a682 100644 --- a/.github/workflows/on_pr.yml +++ b/.github/workflows/on_pr.yml @@ -7,6 +7,13 @@ on: - 'LICENSE' - 'CODEOWNERS' - '.gitignore' + - '.github/workflows/doc*.yml' + - '.github/actions/generate-docs-comment/action.yml' + - '.github/actions/set-deploy-info/action.yml' + - '.github/actions/cleanup-site/action.yml' + - '.github/actions/deploy-docs-preview/action.yml' + - '.github/actions/generate-docs-comment/action.yml' + - '.github/actions/get-changed-docs/action.yml' branches: - main