Skip to content

Docs Preview Post-Build #6

Docs Preview Post-Build

Docs Preview Post-Build #6

name: Docs Preview Post-Build
on:
workflow_run:
workflows: ["Build Docs Preview"]
types: [completed]
permissions:
contents: write
pull-requests: write
actions: read
concurrency:
group: docs-preview-post-build-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false
env:
PREVIEW_RETENTION_LIMIT: 6
jobs:
deploy-preview:
runs-on: ubuntu-24.04
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: true
- name: Download Artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: docs-preview-build
path: temp-artifact
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Read Metadata
id: meta
run: |
ACTION=$(head -n1 temp-artifact/pr/action 2>/dev/null | tr -d '[:space:]')
case "$ACTION" in
opened|reopened|synchronize) ;;
*) echo "::error::invalid or missing action: '$ACTION'"; exit 1 ;;
esac
PR_NUM=$(head -n1 temp-artifact/pr/number 2>/dev/null | tr -d '[:space:]')
case "$PR_NUM" in
''|*[!0-9]*) echo "::error::invalid or missing PR number: '$PR_NUM'"; exit 1 ;;
esac
SHA=$(head -n1 temp-artifact/pr/sha 2>/dev/null | tr -d '[:space:]')
case "$SHA" in
''|*[!a-f0-9]*) echo "::error::invalid or missing commit SHA: '$SHA'"; exit 1 ;;
esac
echo "action=$ACTION" >> "$GITHUB_OUTPUT"
echo "pr_number=$PR_NUM" >> "$GITHUB_OUTPUT"
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
- name: Deploy PR preview
uses: rossjrw/pr-preview-action@ffa7509e91a3ec8dfc2e5536c4d5c1acdf7a6de9 # v1.8.1
with:
source-dir: ./temp-artifact/public
preview-branch: gh-pages
umbrella-dir: pr-preview
pr-number: ${{ steps.meta.outputs.pr_number }}
pages-base-url: docs.layer5.io
action: deploy
wait-for-pages-deployment: false
comment: false
- name: Comment PR with Preview URL
uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5
with:
header: pr-preview
number: ${{ steps.meta.outputs.pr_number }}
message: |
🚀 Preview deployment: https://docs.layer5.io/pr-preview/pr-${{ steps.meta.outputs.pr_number }}/
> *Note: Preview may take a moment (GitHub Pages deployment in progress). Please wait and refresh. Track deployment [here](https://github.com/${{ github.repository }}/actions/workflows/pages/pages-build-deployment)*
prune:
# Both jobs push to gh-pages, but only prune retries a lost race, so deploy
# must land first.
needs: deploy-preview
runs-on: ubuntu-24.04
outputs:
removed_prs: ${{ steps.prune-previews.outputs.removed_prs }}
removed_prs_json: ${{ steps.prune-previews.outputs.removed_prs_json }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout gh-pages for preview retention
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: gh-pages
fetch-depth: 0
persist-credentials: true # required: the prune step pushes to gh-pages
sparse-checkout: |
pr-preview
path: gh-pages-maintenance
- name: Prune old PR previews
id: prune-previews
# Pruning is best-effort housekeeping on the shared gh-pages branch.
# Many preview runs write to gh-pages at once, so it must never be
# the reason a preview is reported as failed: the push is retried against
# the freshest state, and the step is continue-on-error as a backstop.
continue-on-error: true
run: |
cd gh-pages-maintenance
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Write safe defaults first. $GITHUB_OUTPUT is last-write-wins, so even
# if this step is interrupted, the downstream "Comment on pruned
# previews" step sees a valid empty list rather than an empty string.
{
echo "removed_prs="
echo "removed_prs_json=[]"
} >> "$GITHUB_OUTPUT"
# A naive push to the shared gh-pages branch loses the race with other
# concurrent preview runs ("! [rejected] (fetch first)"). Re-derive the
# prune from the freshest remote state on every attempt and retry with
# backoff so a lost race self-heals instead of failing the workflow.
removed_prs=()
outcome=""
attempts=5
for attempt in $(seq 1 "$attempts"); do
git fetch --quiet origin gh-pages
git reset --quiet --hard FETCH_HEAD
mkdir -p pr-preview
mapfile -t previews < <(
while IFS= read -r preview; do
timestamp="$(git log -1 --format=%ct -- "pr-preview/$preview" 2>/dev/null || echo 0)"
printf '%s %s\n' "$timestamp" "$preview"
done < <(find pr-preview -mindepth 1 -maxdepth 1 -type d -name 'pr-*' -printf '%f\n') \
| sort -nr \
| awk '{print $2}'
)
removed_prs=()
if (( ${#previews[@]} > PREVIEW_RETENTION_LIMIT )); then
for preview in "${previews[@]:PREVIEW_RETENTION_LIMIT}"; do
rm -rf "pr-preview/$preview"
removed_prs+=("${preview#pr-}")
done
fi
# Within the retention limit, or another run already pruned: done.
if git diff --quiet -- pr-preview; then
outcome="noop"
break
fi
git add pr-preview
git commit --quiet -m "Prune old PR previews"
if git push origin HEAD:gh-pages; then
outcome="pushed"
break
fi
echo "Prune push lost the race (attempt ${attempt}/${attempts}); re-syncing gh-pages and retrying..."
sleep "$(( attempt * 5 + RANDOM % 5 ))"
done
if [ -z "$outcome" ]; then
echo "::warning::Could not prune old PR previews after ${attempts} attempts due to concurrent gh-pages updates; a later run will retry. Not failing the preview."
removed_prs=()
fi
if [ "${#removed_prs[@]}" -eq 0 ]; then
{
echo "removed_prs="
echo "removed_prs_json=[]"
} >> "$GITHUB_OUTPUT"
else
{
echo "removed_prs=$(IFS=,; echo "${removed_prs[*]}")"
echo "removed_prs_json=$(printf '%s\n' "${removed_prs[@]}" | jq -R . | jq -sc .)"
} >> "$GITHUB_OUTPUT"
fi
- name: Comment on pruned previews
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
REMOVED_PRS_JSON: ${{ steps.prune-previews.outputs.removed_prs_json }}
PREVIEW_RETENTION_LIMIT: ${{ env.PREVIEW_RETENTION_LIMIT }}
with:
script: |
const removedPrs = JSON.parse(process.env.REMOVED_PRS_JSON);
const retentionLimit = process.env.PREVIEW_RETENTION_LIMIT;
const header = "pr-preview";
const marker = `<!-- Sticky Pull Request Comment${header} -->`;
for (const prNumber of removedPrs) {
const body =
`Preview deployment for PR #${prNumber} removed.\n\n` +
`This PR preview was automatically pruned because we keep only the ${retentionLimit} most recently updated previews on GitHub Pages to stay within deployment size limits.\n\n` +
`If needed, push a new commit to this PR to generate a fresh preview.\n` +
`${marker}`;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(prNumber),
per_page: 100,
});
const existingComment = [...comments].reverse().find((comment) =>
comment.user?.login === "github-actions[bot]" &&
comment.body?.includes(marker)
);
if (existingComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existingComment.id,
body,
});
continue;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: Number(prNumber),
body,
});
}