docs(readme): document dual-channel R2 publication workflow #26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Docs Deploy gh-pages | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| publish_source: | |
| description: Select the source used for a manual publication run. | |
| required: true | |
| default: latest-gh-pages | |
| type: choice | |
| options: | |
| - latest-gh-pages | |
| - current-ref-build | |
| concurrency: | |
| group: docs-deploy-gh-pages | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| env: | |
| NODE_VERSION: "22.12.0" | |
| NODE_ENV: production | |
| DOCS_GH_PAGES_PAYLOAD_DIR: .deploy/gh-pages | |
| jobs: | |
| build: | |
| name: Prepare validated publication payload | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout current source ref | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.publish_source == 'current-ref-build' }} | |
| uses: actions/checkout@v4 | |
| - name: Checkout latest gh-pages source | |
| if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.publish_source == 'latest-gh-pages' }} | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: gh-pages | |
| fetch-depth: 1 | |
| - name: Setup Node.js | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.publish_source == 'current-ref-build' }} | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: npm | |
| - name: Install dependencies | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.publish_source == 'current-ref-build' }} | |
| run: npm ci | |
| - name: Build and verify docs | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.publish_source == 'current-ref-build' }} | |
| run: npm run build:ci | |
| env: | |
| CLARITY_PROJECT_ID: ${{ secrets.CLARITY_PROJECT_ID }} | |
| - name: Assemble gh-pages publication payload | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.publish_source == 'current-ref-build' }} | |
| run: | | |
| rm -rf "${DOCS_GH_PAGES_PAYLOAD_DIR}" | |
| mkdir -p "${DOCS_GH_PAGES_PAYLOAD_DIR}/dist" | |
| cp .github/gh-pages/esa.jsonc "${DOCS_GH_PAGES_PAYLOAD_DIR}/esa.jsonc" | |
| cp -R dist/. "${DOCS_GH_PAGES_PAYLOAD_DIR}/dist/" | |
| - name: Assemble payload from latest gh-pages branch | |
| if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.publish_source == 'latest-gh-pages' }} | |
| run: | | |
| if [ ! -f "esa.jsonc" ]; then | |
| echo "::error title=Missing gh-pages payload::The latest gh-pages branch snapshot does not contain esa.jsonc." | |
| exit 1 | |
| fi | |
| if [ ! -d "dist" ]; then | |
| echo "::error title=Missing gh-pages dist::The latest gh-pages branch snapshot does not contain dist/." | |
| exit 1 | |
| fi | |
| rm -rf "${DOCS_GH_PAGES_PAYLOAD_DIR}" | |
| mkdir -p "${DOCS_GH_PAGES_PAYLOAD_DIR}/dist" | |
| cp esa.jsonc "${DOCS_GH_PAGES_PAYLOAD_DIR}/esa.jsonc" | |
| cp -R dist/. "${DOCS_GH_PAGES_PAYLOAD_DIR}/dist/" | |
| echo "Prepared publication payload from the latest gh-pages branch snapshot for manual publication." | |
| - name: Upload validated gh-pages payload | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: docs-gh-pages-payload | |
| path: ${{ env.DOCS_GH_PAGES_PAYLOAD_DIR }} | |
| if-no-files-found: error | |
| deploy: | |
| name: Publish validated snapshot to gh-pages | |
| if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.publish_source == 'current-ref-build' }} | |
| runs-on: ubuntu-latest | |
| needs: build | |
| permissions: | |
| contents: write | |
| environment: | |
| name: docs-production | |
| url: https://docs.hagicode.com | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download validated gh-pages payload | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: docs-gh-pages-payload | |
| path: .deploy/gh-pages | |
| - name: Publish validated gh-pages payload | |
| uses: peaceiris/actions-gh-pages@v4 | |
| with: | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| publish_branch: gh-pages | |
| publish_dir: ./.deploy/gh-pages | |
| force_orphan: true | |
| enable_jekyll: false | |
| - name: Summarize deployment output | |
| run: | | |
| echo "## Publication status" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Publication source: validated build artifact" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- \`gh-pages\` publication: succeeded" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- R2 upload: pending downstream \`upload-r2\` job using the validated \`docs-gh-pages-payload\` artifact" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Published payload: branch-root \`esa.jsonc\` plus \`dist/\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- GitHub environment: docs-production" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Expected public URL: https://docs.hagicode.com" >> "$GITHUB_STEP_SUMMARY" | |
| echo "Published the validated gh-pages payload with ./esa.jsonc and ./dist/." | |
| echo "gh-pages remains authoritative; the downstream R2 upload reuses docs-gh-pages-payload without rebuilding docs." | |
| upload-r2: | |
| name: Upload dist contents to R2 | |
| if: ${{ always() && needs.build.result == 'success' && (needs.deploy.result == 'success' || needs.deploy.result == 'skipped') }} | |
| runs-on: ubuntu-latest | |
| needs: | |
| - build | |
| - deploy | |
| permissions: | |
| contents: read | |
| environment: | |
| name: docs-production | |
| url: https://docs.hagicode.com | |
| steps: | |
| - name: Download validated gh-pages payload | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: docs-gh-pages-payload | |
| path: .deploy/gh-pages | |
| - name: Validate R2 configuration | |
| id: validate-r2 | |
| env: | |
| R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} | |
| R2_BUCKET: ${{ secrets.R2_BUCKET }} | |
| R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| R2_PREFIX: ${{ secrets.R2_PREFIX }} | |
| run: | | |
| required_vars=( | |
| R2_ENDPOINT | |
| R2_BUCKET | |
| R2_ACCESS_KEY_ID | |
| R2_SECRET_ACCESS_KEY | |
| ) | |
| missing=() | |
| for var_name in "${required_vars[@]}"; do | |
| if [ -z "${!var_name}" ]; then | |
| missing+=("${var_name}") | |
| fi | |
| done | |
| normalized_prefix="${R2_PREFIX#/}" | |
| normalized_prefix="${normalized_prefix%/}" | |
| if [ -n "${normalized_prefix}" ]; then | |
| target_root="s3://${R2_BUCKET}/${normalized_prefix}" | |
| prefix_display="${normalized_prefix}" | |
| else | |
| target_root="s3://${R2_BUCKET}" | |
| prefix_display="<bucket-root>" | |
| fi | |
| if [ "${#missing[@]}" -gt 0 ]; then | |
| echo "::error title=Missing R2 configuration::Missing required settings: ${missing[*]}" | |
| echo "R2 upload configuration is incomplete. Missing: ${missing[*]}" | |
| exit 1 | |
| fi | |
| echo "target_root=${target_root}" >> "$GITHUB_OUTPUT" | |
| echo "prefix_display=${prefix_display}" >> "$GITHUB_OUTPUT" | |
| echo "normalized_prefix=${normalized_prefix}" >> "$GITHUB_OUTPUT" | |
| echo "Resolved R2 endpoint: ${R2_ENDPOINT}" | |
| echo "Resolved R2 bucket: ${R2_BUCKET}" | |
| echo "Resolved R2 prefix root: ${prefix_display}" | |
| echo "Uploading .deploy/gh-pages/dist/ contents directly to ${target_root}/ without adding an extra dist/ path segment." | |
| - name: Upload validated dist contents to R2 | |
| id: sync-r2 | |
| env: | |
| R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }} | |
| AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} | |
| AWS_DEFAULT_REGION: auto | |
| AWS_REGION: auto | |
| TARGET_ROOT: ${{ steps.validate-r2.outputs.target_root }} | |
| run: | | |
| echo "Starting R2 sync from .deploy/gh-pages/dist/ to ${TARGET_ROOT}/." | |
| aws s3 sync .deploy/gh-pages/dist/ "${TARGET_ROOT}/" \ | |
| --delete \ | |
| --endpoint-url "${R2_ENDPOINT}" \ | |
| --no-progress | |
| - name: Summarize publication result | |
| if: ${{ always() }} | |
| env: | |
| R2_BUCKET: ${{ secrets.R2_BUCKET }} | |
| R2_PREFIX: ${{ secrets.R2_PREFIX }} | |
| PUBLISH_SOURCE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.publish_source || 'current-ref-build' }} | |
| VALIDATE_OUTCOME: ${{ steps.validate-r2.outcome }} | |
| SYNC_OUTCOME: ${{ steps.sync-r2.outcome }} | |
| TARGET_ROOT: ${{ steps.validate-r2.outputs.target_root }} | |
| PREFIX_DISPLAY: ${{ steps.validate-r2.outputs.prefix_display }} | |
| run: | | |
| normalized_prefix="${R2_PREFIX#/}" | |
| normalized_prefix="${normalized_prefix%/}" | |
| if [ -n "${TARGET_ROOT}" ]; then | |
| resolved_target="${TARGET_ROOT}" | |
| elif [ -n "${normalized_prefix}" ]; then | |
| resolved_target="s3://${R2_BUCKET:-<missing>}/${normalized_prefix}" | |
| else | |
| resolved_target="s3://${R2_BUCKET:-<missing>}" | |
| fi | |
| if [ -n "${PREFIX_DISPLAY}" ]; then | |
| resolved_prefix="${PREFIX_DISPLAY}" | |
| elif [ -n "${normalized_prefix}" ]; then | |
| resolved_prefix="${normalized_prefix}" | |
| else | |
| resolved_prefix="<bucket-root>" | |
| fi | |
| if [ "${PUBLISH_SOURCE}" = "latest-gh-pages" ]; then | |
| gh_pages_status="reused latest gh-pages branch snapshot (manual dispatch)" | |
| publication_mode="manual replay from latest gh-pages" | |
| else | |
| gh_pages_status="succeeded" | |
| publication_mode="validated build artifact" | |
| fi | |
| if [ "${VALIDATE_OUTCOME}" != "success" ]; then | |
| r2_status="failed before transfer" | |
| if [ "${PUBLISH_SOURCE}" = "latest-gh-pages" ]; then | |
| publication_status="Manual publication reused the latest gh-pages branch snapshot, but the R2 upload stopped during configuration validation." | |
| else | |
| publication_status="gh-pages publication succeeded, but the R2 upload stopped during configuration validation." | |
| fi | |
| elif [ "${SYNC_OUTCOME}" = "success" ]; then | |
| r2_status="succeeded" | |
| if [ "${PUBLISH_SOURCE}" = "latest-gh-pages" ]; then | |
| publication_status="Manual publication succeeded: the latest gh-pages branch snapshot was uploaded to R2." | |
| else | |
| publication_status="Dual-channel publication succeeded: gh-pages and R2 are both up to date." | |
| fi | |
| else | |
| r2_status="failed during transfer" | |
| if [ "${PUBLISH_SOURCE}" = "latest-gh-pages" ]; then | |
| publication_status="Manual publication reused the latest gh-pages branch snapshot, but the R2 upload failed during object sync." | |
| else | |
| publication_status="gh-pages publication succeeded, but the R2 upload failed during object sync." | |
| fi | |
| fi | |
| echo "## Publication status" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Publication source: ${publication_mode}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- \`gh-pages\` publication: ${gh_pages_status}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- R2 upload: ${r2_status}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- R2 bucket: \`${R2_BUCKET:-<missing>}\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- R2 prefix root: \`${resolved_prefix}\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Resolved R2 target: \`${resolved_target}\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Upload mapping: \`.deploy/gh-pages/dist/\` contents -> target root (no extra \`dist/\` segment)" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- Result: ${publication_status}" >> "$GITHUB_STEP_SUMMARY" | |
| echo "${publication_status}" | |
| echo "Resolved R2 bucket: ${R2_BUCKET:-<missing>}" | |
| echo "Resolved R2 prefix root: ${resolved_prefix}" | |
| echo "Resolved R2 target: ${resolved_target}" | |
| echo "Upload mapping: .deploy/gh-pages/dist/ contents -> target root (no extra dist/ segment)" |