diff --git a/.github/workflows/delete-preview.yml b/.github/workflows/delete-preview.yml new file mode 100644 index 0000000..9900a75 --- /dev/null +++ b/.github/workflows/delete-preview.yml @@ -0,0 +1,98 @@ +name: Delete PR Preview + +on: + pull_request_target: + types: + - closed + +permissions: + contents: read + +concurrency: + group: publish-pages + cancel-in-progress: false + +jobs: + delete_pr_preview: + runs-on: ubuntu-latest + permissions: + contents: write + pages: write + id-token: write + outputs: + upload_pages: ${{ steps.upload_pages.outcome == 'success' }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Prepare Pages state branch + id: state + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + if git fetch --no-tags --depth=1 origin gh-pages:refs/remotes/origin/gh-pages; then + git worktree add -B gh-pages _pages refs/remotes/origin/gh-pages + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "No gh-pages branch exists yet; nothing to delete." + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Delete PR preview from Pages state + id: delete_preview + if: steps.state.outputs.exists == 'true' + shell: bash + run: | + set -euo pipefail + pr_number="${{ github.event.pull_request.number }}" + + if [ ! -d "_pages/$pr_number" ]; then + echo "Preview directory _pages/$pr_number does not exist." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + rm -rf "_pages/$pr_number" + git -C _pages add -A "$pr_number" + + if git -C _pages diff --cached --quiet; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + git -C _pages commit -m "Delete preview for #$pr_number" + git -C _pages push origin HEAD:gh-pages + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Stage Pages artifact + if: steps.delete_preview.outputs.changed == 'true' + shell: bash + run: | + rm -rf _pages_artifact + mkdir _pages_artifact + cp -a _pages/. _pages_artifact/ + rm -f _pages_artifact/.git + + - name: Upload Pages artifact + if: steps.delete_preview.outputs.changed == 'true' + id: upload_pages + uses: actions/upload-pages-artifact@v3 + with: + path: _pages_artifact + + deploy_after_delete: + needs: delete_pr_preview + if: needs.delete_pr_preview.outputs.upload_pages == 'true' + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: https://lib.cp-algorithms.com/ + steps: + - name: Deploy to GitHub Pages + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1d3792c..dcd94d2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,55 +6,26 @@ on: - verify types: - completed - branches: - - main permissions: contents: read concurrency: - group: "${{ github.workflow }}-main" + group: publish-pages cancel-in-progress: false jobs: - guard: + publish_main: if: > github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' runs-on: ubuntu-latest - permissions: - contents: read - outputs: - current_main: ${{ steps.current_main.outputs.current }} - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.workflow_run.head_sha }} - persist-credentials: false - fetch-depth: 2147483647 - submodules: recursive - - - name: Confirm verified commit is still main - id: current_main - shell: bash - run: | - git fetch --no-tags origin main - if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ]; then - echo "current=true" >> "$GITHUB_OUTPUT" - else - echo "current=false" >> "$GITHUB_OUTPUT" - echo "Skipping stale publish for $(git rev-parse HEAD); main is $(git rev-parse origin/main)." - fi - - publish: - needs: guard - if: needs.guard.outputs.current_main == 'true' - runs-on: ubuntu-latest permissions: actions: read contents: write pages: write + id-token: write outputs: upload_pages: ${{ steps.upload_pages.outcome == 'success' }} steps: @@ -158,16 +129,70 @@ jobs: source: _jekyll destination: _site + - name: Prepare Pages state branch + if: steps.current_main.outputs.current == 'true' && steps.commit_minified.outputs.changed == 'false' + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + if git fetch --no-tags --depth=1 origin gh-pages:refs/remotes/origin/gh-pages; then + git worktree add -B gh-pages _pages refs/remotes/origin/gh-pages + else + git worktree add --detach _pages + git -C _pages switch --orphan gh-pages + git -C _pages rm -rf . || true + fi + + - name: Publish main site into Pages state + if: steps.current_main.outputs.current == 'true' && steps.commit_minified.outputs.changed == 'false' + shell: bash + run: | + set -euo pipefail + shopt -s dotglob nullglob + + for entry in _pages/*; do + name="$(basename "$entry")" + if [ "$name" = ".git" ]; then + continue + fi + if [[ -d "$entry" && "$name" =~ ^[0-9]+$ ]]; then + continue + fi + rm -rf "$entry" + done + + cp -a _site/. _pages/ + touch _pages/.nojekyll + git -C _pages add -A + + if git -C _pages diff --cached --quiet; then + echo "No Pages state changes" + else + git -C _pages commit -m "Publish main site (${{ github.event.workflow_run.head_sha }})" + git -C _pages push origin HEAD:gh-pages + fi + + - name: Stage Pages artifact + if: steps.current_main.outputs.current == 'true' && steps.commit_minified.outputs.changed == 'false' + shell: bash + run: | + rm -rf _pages_artifact + mkdir _pages_artifact + cp -a _pages/. _pages_artifact/ + rm -f _pages_artifact/.git + - name: Upload Pages artifact if: steps.current_main.outputs.current == 'true' && steps.commit_minified.outputs.changed == 'false' id: upload_pages uses: actions/upload-pages-artifact@v3 with: - path: _site + path: _pages_artifact - deploy: - needs: publish - if: needs.publish.outputs.upload_pages == 'true' + deploy_main: + needs: publish_main + if: needs.publish_main.outputs.upload_pages == 'true' runs-on: ubuntu-latest permissions: pages: write @@ -179,3 +204,155 @@ jobs: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 + + publish_preview: + if: > + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + permissions: + actions: read + contents: write + pull-requests: read + pages: write + id-token: write + outputs: + upload_pages: ${{ steps.upload_pages.outcome == 'success' }} + preview_url: ${{ steps.preview.outputs.url }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Download PR preview artifact + uses: actions/download-artifact@v4 + with: + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + pattern: "[0-9]*" + path: public + + - name: Get PR number from artifact + id: preview + shell: bash + run: | + set -euo pipefail + mapfile -t preview_dirs < <(find public -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort) + if [ "${#preview_dirs[@]}" -ne 1 ]; then + printf 'Expected exactly one PR preview artifact, found %s.\n' "${#preview_dirs[@]}" + printf '%s\n' "${preview_dirs[@]}" + exit 1 + fi + pr_number="${preview_dirs[0]}" + echo "number=$pr_number" >> "$GITHUB_OUTPUT" + echo "url=https://lib.cp-algorithms.com/$pr_number/" >> "$GITHUB_OUTPUT" + + - name: Check PR state + id: pr_state + uses: actions/github-script@v7 + with: + script: | + const prNumber = Number('${{ steps.preview.outputs.number }}') + const { owner, repo } = context.repo + const { data: pull } = await github.rest.pulls.get({ + owner, + repo, + pull_number: prNumber, + }) + + const workflowSha = context.payload.workflow_run.head_sha + core.setOutput('state', pull.state) + core.setOutput('head_sha', pull.head.sha) + core.setOutput('publish', pull.state === 'open' && pull.head.sha === workflowSha ? 'true' : 'false') + core.setOutput('delete', pull.state !== 'open' ? 'true' : 'false') + + - name: Prepare Pages state branch + id: pages_state + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + if git fetch --no-tags --depth=1 origin gh-pages:refs/remotes/origin/gh-pages; then + git worktree add -B gh-pages _pages refs/remotes/origin/gh-pages + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "No gh-pages state branch exists yet; run a main publish before deploying previews." + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Update PR preview in Pages state + id: preview_state + if: steps.pages_state.outputs.exists == 'true' + shell: bash + env: + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + PR_HEAD_SHA: ${{ steps.pr_state.outputs.head_sha }} + PR_STATE: ${{ steps.pr_state.outputs.state }} + SHOULD_DELETE: ${{ steps.pr_state.outputs.delete }} + SHOULD_PUBLISH: ${{ steps.pr_state.outputs.publish }} + run: | + set -euo pipefail + pr_number="${{ steps.preview.outputs.number }}" + + if [ "$SHOULD_PUBLISH" = "true" ]; then + rm -rf "_pages/$pr_number" + mkdir -p "_pages/$pr_number" + cp -a "public/$pr_number/." "_pages/$pr_number/" + commit_message="Preview for #$pr_number ($HEAD_SHA) at ${{ steps.preview.outputs.url }}" + elif [ "$SHOULD_DELETE" = "true" ]; then + if [ ! -d "_pages/$pr_number" ]; then + echo "PR #$pr_number is $PR_STATE and no preview directory exists." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + rm -rf "_pages/$pr_number" + commit_message="Delete preview for #$pr_number" + else + echo "Skipping stale preview for #$pr_number; PR head is $PR_HEAD_SHA and workflow head is $HEAD_SHA." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + touch _pages/.nojekyll + git -C _pages add -A "$pr_number" .nojekyll + if git -C _pages diff --cached --quiet; then + echo "No Pages state changes" + echo "changed=false" >> "$GITHUB_OUTPUT" + else + git -C _pages commit -m "$commit_message" + git -C _pages push origin HEAD:gh-pages + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Stage Pages artifact + if: steps.preview_state.outputs.changed == 'true' + shell: bash + run: | + rm -rf _pages_artifact + mkdir _pages_artifact + cp -a _pages/. _pages_artifact/ + rm -f _pages_artifact/.git + + - name: Upload Pages artifact + if: steps.preview_state.outputs.changed == 'true' + id: upload_pages + uses: actions/upload-pages-artifact@v3 + with: + path: _pages_artifact + + deploy_preview: + needs: publish_preview + if: needs.publish_preview.outputs.upload_pages == 'true' + runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ needs.publish_preview.outputs.preview_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 31e8872..8b6cdbc 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -120,11 +120,13 @@ jobs: uses: adamant-pwn/actions/setup@patch-1 with: cache-pip: true + - name: Use GCC 14.2 run: | sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-14 100 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-14 100 g++ --version + - name: Verify uses: competitive-verifier/actions/verify@v2 with: @@ -231,6 +233,31 @@ jobs: path: ${{github.workspace}}/merged-result.json key: ${{ runner.os }}-verify-result-${{ github.sha }} + - name: Configure PR preview base URL + if: github.event_name == 'pull_request' + shell: bash + run: | + { + echo 'url: https://lib.cp-algorithms.com' + echo 'baseurl: "/${{ github.event.pull_request.number }}"' + } >> _jekyll/_config.yml + + - name: Build PR preview with Jekyll + if: github.event_name == 'pull_request' + uses: actions/jekyll-build-pages@v1 + with: + source: _jekyll + destination: _site + + - name: Upload PR preview + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: ${{ github.event.pull_request.number }} + path: _site + if-no-files-found: error + retention-days: 7 + - name: Upload merged result for publish if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-artifact@v4