diff --git a/.github/workflows/generate-top-index.yml b/.github/workflows/generate-top-index.yml index 032efd84b41..ed409f619cb 100644 --- a/.github/workflows/generate-top-index.yml +++ b/.github/workflows/generate-top-index.yml @@ -61,15 +61,25 @@ jobs: git config --local user.email "nextcloud-command@users.noreply.github.com" git config --local user.name "nextcloud-command" - - name: Commit and push changes + - name: Create Pull Request for documentation index deployment if: github.event_name == 'push' - run: | - # Move the generated index.html and static files to the root of the gh-pages branch - rm -rf index.html static/ - mv /tmp/index.html index.html - mv /tmp/static/ static/ - git add index.html static/ - git commit -m "chore: update index.html for documentation" || echo "No changes to commit" - git push origin gh-pages || echo "Nothing to push (expected if no changes)" + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + id: cpr + with: + token: ${{ secrets.COMMAND_BOT_PAT }} + commit-message: 'chore: deploy documentation index for ${{ github.ref_name }}' + committer: nextcloud-command + author: nextcloud-command + signoff: true + branch: 'automated/deploy/documentation-index-${{ github.ref_name }}' + base: gh-pages + title: 'Deploy documentation index for ${{ needs.stage.outputs.branch_name }}' + body: 'Automated documentation index deployment from branch ${{ github.ref_name }}' + delete-branch: true + labels: 'automated, 3. to review' + + - name: Enable Pull Request Automerge + if: github.event_name == 'push' + run: gh pr merge --merge --auto "${{ steps.cpr.outputs.pull-request-number }}" env: GH_TOKEN: ${{ secrets.COMMAND_BOT_PAT }} diff --git a/.github/workflows/sphinxbuild.yml b/.github/workflows/sphinxbuild.yml index e85645e4ffd..c2970898ded 100644 --- a/.github/workflows/sphinxbuild.yml +++ b/.github/workflows/sphinxbuild.yml @@ -11,10 +11,42 @@ permissions: contents: read jobs: - build: + setup-latex-cache: + name: Cache LaTeX packages + runs-on: ubuntu-latest + steps: + - name: Configure apt cache + run: | + mkdir -p /tmp/apt/cache/archives + echo 'Dir::Cache::archives "/tmp/apt/cache/archives";' | sudo tee /etc/apt/apt.conf.d/apt-cache-tmp + + - name: Cache LaTeX apt packages + id: cache-latex-apt + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: /tmp/apt/cache/archives + key: latex-apt-${{ runner.os }}-${{ runner.arch }}-ubuntu-24.04-texlive-2023 + restore-keys: | + latex-apt-${{ runner.os }}-${{ runner.arch }}-ubuntu-24.04- + latex-apt-${{ runner.os }}-${{ runner.arch }}- + + - name: Download LaTeX packages (cache miss only) + if: steps.cache-latex-apt.outputs.cache-hit != 'true' + run: | + sudo DEBIAN_FRONTEND=noninteractive apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + --download-only \ + python3-pil python3-pip texlive-fonts-recommended latexmk \ + texlive-latex-extra texlive-latex-recommended texlive-xetex \ + texlive-fonts-extra-links texlive-fonts-extra xindy + # Ensure downloaded packages are owned by the current user so they can be cached + sudo chown -R $(id -u):$(id -g) /tmp/apt/cache/archives + + build: name: Build ${{ matrix.manual.name }} runs-on: ubuntu-latest + needs: setup-latex-cache strategy: matrix: @@ -46,218 +78,300 @@ jobs: publish: true steps: - - name: Cache git metadata - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: .git - key: git-metadata-${{ github.sha }} - restore-keys: | - git-metadata-${{ github.sha }} - git-metadata - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Configure apt cache to use /dev/shm - if: ${{ matrix.manual.build_pdf_path }} - run: | - mkdir -p /dev/shm/apt/cache/archives - echo 'Dir::Cache::archives "/dev/shm/apt/cache/archives";' | sudo tee /etc/apt/apt.conf.d/apt-cache-shm - - - name: Cache LaTeX apt packages - if: ${{ matrix.manual.build_pdf_path }} - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: /dev/shm/apt/cache/archives - key: latex-apt-${{ runner.os }}-${{ hashFiles('.github/workflows/sphinxbuild.yml') }} - restore-keys: | - latex-apt-${{ runner.os }}- - - - name: Install LaTeX dependencies - if: ${{ matrix.manual.build_pdf_path }} - run: | - sudo DEBIAN_FRONTEND=noninteractive apt-get update - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \ - --no-install-recommends \ - python3-pil \ - python3-pip \ - texlive-fonts-recommended \ - latexmk \ - texlive-latex-extra \ - texlive-latex-recommended \ - texlive-xetex \ - texlive-fonts-extra-links \ - texlive-fonts-extra \ - xindy - - - name: Fix apt cache ownership for caching - if: ${{ matrix.manual.build_pdf_path }} - run: sudo chown -R $(id -u):$(id -g) /dev/shm/apt/cache/archives - - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: '3.12' - cache: 'pip' - - - name: Install pip dependencies - run: pip install -r requirements.txt - - - name: Build html documentation - run: cd ${{ matrix.manual.directory }} && make ${{ matrix.manual.make_target }} - - - name: Compute PDF release version - if: ${{ matrix.manual.build_pdf_path }} - id: pdf_version - run: | - branch="${GITHUB_REF#refs/heads/}" - if [[ "$branch" == stable* ]]; then - echo "release=${branch#stable}" >> $GITHUB_OUTPUT - else - echo "release=latest" >> $GITHUB_OUTPUT - fi - - - name: Build pdf documentation - if: ${{ matrix.manual.build_pdf_path }} - env: - DOCS_RELEASE: ${{ steps.pdf_version.outputs.release }} - run: | - set -e - cd ${{ matrix.manual.directory }} - make latexpdf - ls -la ${{ matrix.manual.build_pdf_path }} - cp ${{ matrix.manual.build_pdf_path }}/*.pdf ${{ matrix.manual.build_path }}/ - - - name: Upload static documentation - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - if: ${{ matrix.manual.publish }} - with: - name: ${{ matrix.manual.name }} - path: ${{ matrix.manual.directory }}/${{ matrix.manual.build_path }} + - name: Cache git metadata + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: .git + key: git-metadata-${{ github.sha }} + restore-keys: | + git-metadata-${{ github.sha }} + git-metadata + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + cache: "pip" + + - name: Install pip dependencies + run: pip install -r requirements.txt + + - name: Configure apt cache + if: ${{ matrix.manual.build_pdf_path }} + run: | + mkdir -p /tmp/apt/cache/archives + echo 'Dir::Cache::archives "/tmp/apt/cache/archives";' | sudo tee /etc/apt/apt.conf.d/apt-cache-tmp + + - name: Restore LaTeX apt cache + if: ${{ matrix.manual.build_pdf_path }} + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: /tmp/apt/cache/archives + key: latex-apt-${{ runner.os }}-${{ runner.arch }}-ubuntu-24.04-texlive-2023 + restore-keys: | + latex-apt-${{ runner.os }}-${{ runner.arch }}-ubuntu-24.04- + latex-apt-${{ runner.os }}-${{ runner.arch }}- + + - name: Install LaTeX from cache + if: ${{ matrix.manual.build_pdf_path }} + run: | + sudo DEBIAN_FRONTEND=noninteractive apt-get update + sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + --no-download \ + python3-pil python3-pip texlive-fonts-recommended latexmk \ + texlive-latex-extra texlive-latex-recommended texlive-xetex \ + texlive-fonts-extra-links texlive-fonts-extra xindy + + - name: Build html documentation + run: cd ${{ matrix.manual.directory }} && make ${{ matrix.manual.make_target }} + + - name: Compute PDF release version + if: ${{ matrix.manual.build_pdf_path }} + id: pdf_version + run: | + branch="${GITHUB_REF#refs/heads/}" + if [[ "$branch" == stable* ]]; then + echo "release=${branch#stable}" >> $GITHUB_OUTPUT + else + echo "release=latest" >> $GITHUB_OUTPUT + fi - deploy: - name: Deploy pages + - name: Build pdf documentation + if: ${{ matrix.manual.build_pdf_path }} + env: + DOCS_RELEASE: ${{ steps.pdf_version.outputs.release }} + run: | + set -e + cd ${{ matrix.manual.directory }} + make latexpdf + ls -la ${{ matrix.manual.build_pdf_path }} + cp ${{ matrix.manual.build_pdf_path }}/*.pdf ${{ matrix.manual.build_path }}/ + + - name: Upload static documentation + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: ${{ matrix.manual.publish }} + with: + name: ${{ matrix.manual.name }} + path: ${{ matrix.manual.directory }}/${{ matrix.manual.build_path }} + + # Assembles all artifacts into the gh-pages layout and uploads a single + # "staged" artifact that both `check` and `deploy` consume. Doing the + # assembly once avoids repeating the checkout + merge logic in both jobs. + stage: + name: Stage documentation needs: build runs-on: ubuntu-latest - if: github.event_name == 'push' # Only deploy on push, not PR + + outputs: + branch_name: ${{ steps.branch.outputs.branch_name }} + version_name: ${{ steps.branch.outputs.version_name }} + + steps: + - name: Cache git metadata + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: .git + key: git-metadata-${{ github.sha }} + restore-keys: | + git-metadata-${{ github.sha }} + git-metadata + + - name: Checkout Github Pages branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: gh-pages + fetch-depth: 0 + + - name: Download all artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: artifacts/ + + - name: Get branch name and find latest stable + id: branch + run: | + current_branch="${GITHUB_REF#refs/heads/}" + + # Find the highest numbered stable branch from the remote + highest_stable=$(git ls-remote --heads origin | sed -n 's?.*refs/heads/stable\([0-9]\{2\}\)$?\1?p' | sort -n | tail -1) + highest_stable_branch="stable${highest_stable}" + + echo "Current branch: $current_branch" + echo "Highest stable branch found: $highest_stable_branch" + + # Map actual branch names to deployment folder names + case "$current_branch" in + "master") + echo "branch_name=latest" >> $GITHUB_OUTPUT + ;; + "$highest_stable_branch") + echo "branch_name=stable" >> $GITHUB_OUTPUT + # Also record the numeric version so we can publish to server// too + echo "version_name=${highest_stable}" >> $GITHUB_OUTPUT + ;; + *) + # Remove stable prefix for current branch + current_branch="${current_branch#stable}" + echo "branch_name=$current_branch" >> $GITHUB_OUTPUT + ;; + esac + + echo "Deployment folder name: ${{ steps.branch.outputs.branch_name }}" + echo "Version name for additional deployment (if applicable): ${{ steps.branch.outputs.version_name }}" + + - name: Merge ${{ steps.branch.outputs.branch_name }} documentation artifacts into gh-pages + run: | + # List artifacts + ls -la artifacts/*/ + + # Cleanup old documentation + rm -rf ${{ steps.branch.outputs.branch_name }} + rm -rf server/${{ steps.branch.outputs.branch_name }} + mkdir -p server/${{ steps.branch.outputs.branch_name }} + + # Copy all built documentation into dedicated subdirectories + for artifact in artifacts/*; do + if [ -d "$artifact" ]; then + manual_name="$(basename "$artifact")" + mkdir -p "server/${{ steps.branch.outputs.branch_name }}/$manual_name" + cp -r "$artifact/"* "server/${{ steps.branch.outputs.branch_name }}/$manual_name/" + fi + done + + # Move pdf files to the root of the branch_name + mv server/${{ steps.branch.outputs.branch_name }}/*/*.pdf server/${{ steps.branch.outputs.branch_name }}/ || true + + # If this is the highest stable branch, also deploy to its versioned folder + if [ -n "${{ steps.branch.outputs.version_name }}" ]; then + rm -rf server/${{ steps.branch.outputs.version_name }} + cp -r server/${{ steps.branch.outputs.branch_name }} server/${{ steps.branch.outputs.version_name }} + fi + + # Cleanup + find . -type d -empty -delete + rm -rf artifacts + + - name: Add various redirects for go.php and user_manual english version + run: | + # Fetch source branches so git checkout origin/... works from the gh-pages checkout + git fetch origin ${{ github.event.repository.default_branch }} ${{ github.ref_name }} + + # Generate go.php redirect from main branch + git checkout origin/${{ github.event.repository.default_branch }} -- go.php/index.html + mkdir -p server/${{ steps.branch.outputs.branch_name }}/go.php + mv go.php/index.html server/${{ steps.branch.outputs.branch_name }}/go.php/index.html + + # Generate user_manual english redirect + git checkout origin/${{ github.ref_name }} -- user_manual/index.html + mkdir -p server/${{ steps.branch.outputs.branch_name }}/user_manual + mv user_manual/index.html server/${{ steps.branch.outputs.branch_name }}/user_manual/index.html + + - name: Upload staged site + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: staged-site + path: server/ + + check: + name: Check staged documentation + needs: stage + runs-on: ubuntu-latest + + steps: + - name: Download staged site + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: staged-site + path: server/ + + - name: Check for broken links with lychee + uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0 + with: + fail: true + token: ${{ secrets.GITHUB_TOKEN }} + jobSummary: true + args: | + --offline --no-progress --verbose + --remap "https://docs.nextcloud.com/server/${{ needs.stage.outputs.branch_name }} file://$(pwd)/server/${{ needs.stage.outputs.branch_name }}/" + 'server/${{ needs.stage.outputs.branch_name }}/**/*.html' + + deploy: + name: Deploy documentation to gh-pages + needs: [stage, check] + runs-on: ubuntu-latest + # if: github.event_name == 'push' permissions: contents: write + pull-requests: write steps: - - name: Cache git metadata - uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 - with: - path: .git - key: git-metadata-${{ github.sha }} - restore-keys: | - git-metadata-${{ github.sha }} - git-metadata - - - name: Checkout Github Pages branch - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: gh-pages - fetch-depth: 0 - token: ${{ secrets.COMMAND_BOT_PAT }} - - - name: Download all ${{ needs.build.outputs.branch_name }} artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - path: artifacts/ - - - name: Get branch name and find latest stable - id: branch - run: | - current_branch="${GITHUB_REF#refs/heads/}" - - # Find the highest numbered stable branch from the remote - highest_stable=$(git ls-remote --heads origin | sed -n 's?.*refs/heads/stable\([0-9]\{2\}\)$?\1?p' | sort -n | tail -1) - highest_stable_branch="stable${highest_stable}" - - echo "Current branch: $current_branch" - echo "Highest stable branch found: $highest_stable_branch" - - # Map actual branch names to deployment folder names - case "$current_branch" in - "master") - echo "branch_name=latest" >> $GITHUB_OUTPUT - ;; - "$highest_stable_branch") - echo "branch_name=stable" >> $GITHUB_OUTPUT - # Also record the numeric version so we can publish to server// too - echo "version_name=${highest_stable}" >> $GITHUB_OUTPUT - ;; - *) - # Remove stable prefix for current branch - current_branch="${current_branch#stable}" - echo "branch_name=$current_branch" >> $GITHUB_OUTPUT - ;; - esac - - echo "Deployment folder name: ${{ steps.branch.outputs.branch_name }}" - echo "Version name for additional deployment (if applicable): ${{ steps.branch.outputs.version_name }}" - - - name: Merge ${{ steps.branch.outputs.branch_name }} documentation artifacts into gh-pages - run: | - # List artifacts - ls -la artifacts/*/ - - # Cleanup old documentation - rm -rf ${{ steps.branch.outputs.branch_name }} - rm -rf server/${{ steps.branch.outputs.branch_name }} - mkdir -p server/${{ steps.branch.outputs.branch_name }} - - # Copy all built documentation into dedicated subdirectories - for artifact in artifacts/*; do - if [ -d "$artifact" ]; then - manual_name="$(basename "$artifact")" - mkdir -p "server/${{ steps.branch.outputs.branch_name }}/$manual_name" - cp -r "$artifact/"* "server/${{ steps.branch.outputs.branch_name }}/$manual_name/" + - name: Cache git metadata + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: .git + key: git-metadata-${{ github.sha }} + restore-keys: | + git-metadata-${{ github.sha }} + git-metadata + + - name: Checkout gh-pages branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: gh-pages + fetch-depth: 0 + persist-credentials: false + + - name: Download staged site + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: staged-site + path: incoming/ + + - name: Apply staged site to gh-pages working tree + run: | + branch="${{ needs.stage.outputs.branch_name }}" + version="${{ needs.stage.outputs.version_name }}" + + rm -rf "server/${branch}" + cp -r "incoming/${branch}" "server/${branch}" + + if [ -n "${version}" ]; then + rm -rf "server/${version}" + cp -r "incoming/${version}" "server/${version}" fi - done - - # Move pdf files to the root of the branch_name - mv server/${{ steps.branch.outputs.branch_name }}/*/*.pdf server/${{ steps.branch.outputs.branch_name }}/ || true - - # If this is the highest stable branch, also deploy to its versioned folder - if [ -n "${{ steps.branch.outputs.version_name }}" ]; then - rm -rf server/${{ steps.branch.outputs.version_name }} - cp -r server/${{ steps.branch.outputs.branch_name }} server/${{ steps.branch.outputs.version_name }} - fi - - # Cleanup - find . -type d -empty -delete - rm -rf artifacts - - - name: Add various redirects for go.php and user_manual english version - run: | - # Fetch source branches so git checkout origin/... works from the gh-pages checkout - git fetch origin ${{ github.event.repository.default_branch }} ${{ github.ref_name }} - - # Generate go.php redirect from main branch - git checkout origin/${{ github.event.repository.default_branch }} -- go.php/index.html - mkdir -p server/${{ steps.branch.outputs.branch_name }}/go.php - mv go.php/index.html server/${{ steps.branch.outputs.branch_name }}/go.php/index.html - - # Generate user_manual english redirect - git checkout origin/${{ github.ref_name }} -- user_manual/index.html - mkdir -p server/${{ steps.branch.outputs.branch_name }}/user_manual - mv user_manual/index.html server/${{ steps.branch.outputs.branch_name }}/user_manual/index.html - - - name: Commit ${{ steps.branch.outputs.branch_name }} documentation and push to gh-pages - run: | - git config --local user.email "nextcloud-command@users.noreply.github.com" - git config --local user.name "nextcloud-command" - git add . - git diff --staged --quiet || git commit -m "chore: deploy documentation for ${{ steps.branch.outputs.branch_name }}" - # Ensure we are up to date with the remote gh-pages branch - git pull --rebase origin gh-pages || true - git push origin gh-pages || echo "Nothing to push (expected if no changes)" - env: - GH_TOKEN: ${{ secrets.COMMAND_BOT_PAT }} + + find . -type d -empty -delete + + # Quickly log the directory structure for debugging purposes + echo "Directory structure after applying staged site:" + find server -type d -print + + - name: Create Pull Request for documentation deployment + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + id: cpr + with: + token: ${{ secrets.COMMAND_BOT_PAT }} + commit-message: "chore: deploy documentation for ${{ needs.stage.outputs.branch_name }}" + committer: nextcloud-command + author: nextcloud-command + signoff: true + branch: "automated/deploy/documentation-${{ needs.stage.outputs.branch_name }}" + base: gh-pages + title: "Deploy documentation for ${{ needs.stage.outputs.branch_name }}" + body: "Automated documentation deployment from branch ${{ github.ref_name }}" + delete-branch: true + labels: "automated, 3. to review" + + - name: Enable Pull Request Automerge + run: gh pr merge --merge --auto "${{ steps.cpr.outputs.pull-request-number }}" + env: + GH_TOKEN: ${{ secrets.COMMAND_BOT_PAT }} summary: - needs: build + needs: [build, check] runs-on: ubuntu-latest-low if: always() @@ -267,6 +381,5 @@ jobs: name: build-deploy-summary steps: - # Only check if the build was successful - name: Summary status - run: if ${{ needs.build.result != 'success' }}; then exit 1; fi + run: if ${{ needs.build.result != 'success' || needs.check.result != 'success' }}; then exit 1; fi diff --git a/.github/workflows/update-stable-titles.yml b/.github/workflows/update-stable-titles.yml new file mode 100644 index 00000000000..565e9bcc718 --- /dev/null +++ b/.github/workflows/update-stable-titles.yml @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT +name: Update PRs titles on stable branches + +on: + pull_request: + types: [opened, edited] + branches: + - "stable*" + +concurrency: + group: stable-pr-title-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + update-pr-title: + runs-on: ubuntu-latest-low + permissions: + pull-requests: write + contents: read + + steps: + - name: Wait for potential title edits + run: sleep 15 + + - name: Get PR details and update title + # Renovate already have target branch in the title + if: github.event.pull_request.user.login != 'renovate[bot]' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + const baseBranch = pr.base.ref; + const currentTitle = pr.title; + + // Check if this is a stable branch + // Should not happen as we only trigger on stable* branches 🤷‍♀️ + if (!baseBranch.startsWith('stable')) { + console.log(`Not a stable branch: ${baseBranch}`); + return; + } + + const prefix = `[${baseBranch}]`; + + // Check if title already has the correct prefix and no other stable tags + const correctTagRegex = new RegExp(`^\\[${baseBranch}\\]\\s*`); + const hasOtherStableTags = /\[stable[\d.]*\]/.test(currentTitle.replace(correctTagRegex, '')); + + if (correctTagRegex.test(currentTitle) && !hasOtherStableTags) { + console.log(`Title already has correct prefix only: ${currentTitle}`); + return; + } + + // Remove all stable tags and add the correct one + const cleanTitle = currentTitle.replace(/\[stable[\d.]*\]\s*/g, '').trim(); + const newTitle = `${prefix} ${cleanTitle}`; + + console.log(`Updating title from: "${currentTitle}" to: "${newTitle}"`); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + title: newTitle, + });