From c10fd585848ea2f96c129cbb118709eec1ba9531 Mon Sep 17 00:00:00 2001 From: Idogwu Chinonso Date: Fri, 5 Jun 2026 01:33:47 +0100 Subject: [PATCH] ci: production deploy to GitHub Pages + domain-aware previews - deploy-prod.yml: build on push to main and publish to the gh-pages root, preserving pr-preview/ so PR previews keep working on the same branch. - preview workflows: when a root custom domain is configured (the PAGES_CNAME repo variable), previews build at /pr-preview/pr-N/ and the comment links there; otherwise unchanged. Environment (URL / baseUrl / CNAME) is driven by repo Actions variables, so the same workflows serve the github.io test, a staging domain, and production with no edits. --- .github/workflows/deploy-prod.yml | 93 ++++++++++++++++++++++++++++ .github/workflows/preview-build.yml | 11 ++-- .github/workflows/preview-deploy.yml | 11 +++- 3 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/deploy-prod.yml diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 00000000..56f787ca --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,93 @@ +name: deploy-prod + +# Builds the docs on every push to the default branch and publishes them to the +# ROOT of the gh-pages branch — the production site — while preserving any +# pr-preview/ directories so PR previews are never clobbered. +# +# Runs in the trusted base-repo context (push to the default branch), so it has +# write access and needs no fork-safety split. +# +# Environment is driven by repo Actions variables, so the SAME workflow serves the +# github.io sub-path, a staging custom domain, and production with no code edits: +# vars.DOCS_URL default: https://.github.io +# vars.DOCS_BASE_URL default: // (set to "/" for a root custom domain) +# vars.PAGES_CNAME optional: when set, written as the gh-pages CNAME each deploy + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + +# Shared with preview-deploy so prod and preview writes to gh-pages never race. +concurrency: + group: gh-pages-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: yarn + + - run: yarn install + + - name: Build production site + env: + DOCS_URL: ${{ vars.DOCS_URL || format('https://{0}.github.io', github.repository_owner) }} + DOCS_BASE_URL: ${{ vars.DOCS_BASE_URL || format('/{0}/', github.event.repository.name) }} + run: yarn build + + - name: Publish to gh-pages root (preserve pr-preview/) + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PAGES_CNAME: ${{ vars.PAGES_CNAME }} + run: | + set -euo pipefail + REPO_URL="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + + if git clone --quiet --depth 1 --branch gh-pages --single-branch "$REPO_URL" gh-pages 2>/dev/null; then + cd gh-pages + else + mkdir gh-pages && cd gh-pages + git init --quiet + git remote add origin "$REPO_URL" + git checkout --quiet --orphan gh-pages + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + # Replace the production site at the root while keeping pr-preview/ (and .git). + # Defined as a function so it can be replayed verbatim on a push retry. + publish() { + find . -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'pr-preview' -exec rm -rf {} + + cp -R "${GITHUB_WORKSPACE}/build/." . + touch .nojekyll # serve Docusaurus output verbatim (skip GitHub Pages' Jekyll) + if [ -n "${PAGES_CNAME:-}" ]; then echo "$PAGES_CNAME" > CNAME; fi + git add -A + } + + publish + if git diff --quiet --cached; then + echo "No changes to publish." + exit 0 + fi + git commit --quiet -m "Deploy production ($GITHUB_SHA)" + + # Serialized by the concurrency group; if the branch moved anyway, adopt the + # latest tree and replay (never touching pr-preview/) before retrying. + if ! git push --quiet origin gh-pages; then + echo "Push rejected; re-syncing gh-pages and retrying." + git fetch --quiet --depth 1 origin gh-pages + git reset --hard origin/gh-pages + publish + git diff --quiet --cached || git commit --quiet -m "Deploy production ($GITHUB_SHA)" + git push --quiet origin gh-pages + fi diff --git a/.github/workflows/preview-build.yml b/.github/workflows/preview-build.yml index 94a50f12..4ecadb69 100644 --- a/.github/workflows/preview-build.yml +++ b/.github/workflows/preview-build.yml @@ -66,10 +66,13 @@ jobs: - name: Build Docusaurus site if: github.event.action != 'closed' env: - # Point this build at the GitHub Pages sub-path the PR will be published to. - # The defaults in docusaurus.config.js keep production (Amplify) unchanged. - DOCS_URL: https://${{ github.repository_owner }}.github.io - DOCS_BASE_URL: /${{ github.event.repository.name }}/pr-preview/pr-${{ github.event.number }}/ + # Point this build at the path the PR will be published to. When a root custom + # domain is configured (the PAGES_CNAME repo variable), previews live at + # /pr-preview/pr-/; otherwise at the github.io project sub-path + # .github.io//pr-preview/pr-/. Defaults in docusaurus.config.js + # keep production builds unchanged. + DOCS_URL: ${{ vars.PAGES_CNAME && format('https://{0}', vars.PAGES_CNAME) || format('https://{0}.github.io', github.repository_owner) }} + DOCS_BASE_URL: ${{ vars.PAGES_CNAME && format('/pr-preview/pr-{0}/', github.event.number) || format('/{0}/pr-preview/pr-{1}/', github.event.repository.name, github.event.number) }} run: yarn build - name: Upload site artifact diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml index 479e3bba..4ed6f1b1 100644 --- a/.github/workflows/preview-deploy.yml +++ b/.github/workflows/preview-deploy.yml @@ -139,12 +139,19 @@ jobs: PR_NUMBER: ${{ steps.meta.outputs.number }} PR_ACTION: ${{ steps.meta.outputs.action }} SRC_SHA: ${{ github.event.workflow_run.head_sha }} + PAGES_CNAME: ${{ vars.PAGES_CNAME }} run: | set -euo pipefail OWNER="${GITHUB_REPOSITORY%%/*}" REPO="${GITHUB_REPOSITORY##*/}" - OWNER_LC="$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')" - URL="https://${OWNER_LC}.github.io/${REPO}/pr-preview/pr-${PR_NUMBER}/" + # When a root custom domain is configured, previews are served at + # /pr-preview/pr-/; otherwise at the github.io project sub-path. + if [ -n "${PAGES_CNAME:-}" ]; then + URL="https://${PAGES_CNAME}/pr-preview/pr-${PR_NUMBER}/" + else + OWNER_LC="$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')" + URL="https://${OWNER_LC}.github.io/${REPO}/pr-preview/pr-${PR_NUMBER}/" + fi SHORT_SHA="${SRC_SHA:0:7}" MARKER=""