|
| 1 | +name: Docs |
| 2 | + |
| 3 | +# Triggers: |
| 4 | +# - push to main touching docs/** or this workflow → deploy latest to GitHub Pages |
| 5 | +# - push of a major release tag (v2.0.0, v3.0.0 …) → versioned snapshot |
| 6 | +# - workflow_dispatch → manual deploy of latest |
| 7 | +on: |
| 8 | + push: |
| 9 | + branches: [main] |
| 10 | + paths: |
| 11 | + - "docs/**" |
| 12 | + - ".github/workflows/docs.yml" |
| 13 | + tags: |
| 14 | + # Glob pattern (not regex): matches v1.0.0, v2.0.0, v10.0.0 … |
| 15 | + - "v[0-9]*.0.0" |
| 16 | + workflow_dispatch: |
| 17 | + |
| 18 | +# Deployment strategy: GitHub Actions Pages source (actions/deploy-pages — official, no third party). |
| 19 | +# • gh-pages branch = versioned snapshot STORAGE only (not served directly by Pages) |
| 20 | +# • Every deploy job assembles a combined artifact: |
| 21 | +# - latest docs built from main at the artifact root |
| 22 | +# - each vX/ snapshot from the gh-pages branch merged in |
| 23 | +# → single artifact deployed via actions/deploy-pages |
| 24 | +# • Snapshot job stores the built snapshot in gh-pages branch via plain git, |
| 25 | +# then pushes the updated versions.json to main (no [skip ci]) which |
| 26 | +# re-triggers the deploy job to include the new snapshot immediately. |
| 27 | +# Requires: Settings → Pages → Source: GitHub Actions. |
| 28 | +permissions: |
| 29 | + contents: write |
| 30 | + pages: write |
| 31 | + id-token: write |
| 32 | + |
| 33 | +# Only one concurrent deployment for the same ref; cancel outdated runs. |
| 34 | +concurrency: |
| 35 | + group: docs-${{ github.ref }} |
| 36 | + cancel-in-progress: true |
| 37 | + |
| 38 | +jobs: |
| 39 | + # ── Deploy (latest) ───────────────────────────────────────────────────────── |
| 40 | + # Assembles a combined Pages artifact: |
| 41 | + # 1. Builds the latest docs (base: /github-code-search/) |
| 42 | + # 2. Merges existing versioned snapshots from the gh-pages storage branch |
| 43 | + # into the artifact (docs/.vitepress/dist/vX/ for each stored version) |
| 44 | + # 3. Uploads the artifact and deploys via actions/deploy-pages |
| 45 | + # Requires: Settings → Pages → Source: GitHub Actions. |
| 46 | + deploy: |
| 47 | + name: Build and deploy docs |
| 48 | + if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' |
| 49 | + runs-on: ubuntu-latest |
| 50 | + permissions: |
| 51 | + contents: read |
| 52 | + pages: write |
| 53 | + id-token: write |
| 54 | + environment: |
| 55 | + name: github-pages |
| 56 | + url: ${{ steps.deploy.outputs.page_url }} |
| 57 | + |
| 58 | + steps: |
| 59 | + - name: Checkout |
| 60 | + uses: actions/checkout@v4.2.2 |
| 61 | + with: |
| 62 | + # Needed to fetch the gh-pages storage branch for versioned snapshots. |
| 63 | + fetch-depth: 0 |
| 64 | + |
| 65 | + - name: Setup Bun |
| 66 | + uses: oven-sh/setup-bun@v2.1.2 |
| 67 | + with: |
| 68 | + bun-version: latest |
| 69 | + |
| 70 | + - name: Install dependencies |
| 71 | + run: bun install --frozen-lockfile |
| 72 | + |
| 73 | + - name: Build latest docs |
| 74 | + run: bun run docs:build |
| 75 | + # Base URL: /github-code-search/ (default in config.mts) |
| 76 | + |
| 77 | + - name: Merge versioned snapshots from gh-pages storage |
| 78 | + run: | |
| 79 | + set -euo pipefail |
| 80 | + if git ls-remote --heads origin gh-pages | grep -q gh-pages; then |
| 81 | + git fetch origin gh-pages |
| 82 | + # List only top-level entries that match v<integer> (e.g. v1, v2). |
| 83 | + # Using a pathspec glob and a strict regex avoids iterating over |
| 84 | + # unrelated files (.nojekyll, README, etc.) at the branch root. |
| 85 | + for entry in $(git ls-tree --name-only origin/gh-pages -- 'v[0-9]*'); do |
| 86 | + if echo "$entry" | grep -qE '^v[0-9]+$'; then |
| 87 | + echo "Merging snapshot: $entry" |
| 88 | + mkdir -p "docs/.vitepress/dist/$entry" |
| 89 | + git archive origin/gh-pages "$entry" | tar -x -C docs/.vitepress/dist/ |
| 90 | + # Validate: warn and remove if the extracted directory is empty. |
| 91 | + if [ -z "$(ls -A "docs/.vitepress/dist/$entry" 2>/dev/null)" ]; then |
| 92 | + echo "Warning: snapshot '$entry' is empty after extraction — removing." |
| 93 | + rmdir "docs/.vitepress/dist/$entry" |
| 94 | + fi |
| 95 | + fi |
| 96 | + done |
| 97 | + else |
| 98 | + echo "No gh-pages branch yet — skipping snapshot merge" |
| 99 | + fi |
| 100 | +
|
| 101 | + - name: Upload Pages artifact |
| 102 | + uses: actions/upload-pages-artifact@v3.0.1 |
| 103 | + with: |
| 104 | + path: docs/.vitepress/dist |
| 105 | + |
| 106 | + - name: Deploy to GitHub Pages |
| 107 | + id: deploy |
| 108 | + uses: actions/deploy-pages@v4.0.5 |
| 109 | + |
| 110 | + # ── Snapshot (versioned) ──────────────────────────────────────────────────── |
| 111 | + # Triggered by a major release tag (e.g. v2.0.0). |
| 112 | + # 1. Builds docs with a versioned base URL (/github-code-search/v2/). |
| 113 | + # 2. Stores the output in the gh-pages branch under /v2/ using plain git |
| 114 | + # (no third-party action). The gh-pages branch is storage only — Pages |
| 115 | + # still points to GitHub Actions; the deploy job merges snapshots in. |
| 116 | + # 3. Prepends the entry to versions.json on main WITHOUT [skip ci], which |
| 117 | + # re-triggers the deploy job so the new snapshot is live immediately. |
| 118 | + # |
| 119 | + # Convention: only tags matching vX.0.0 (major bumps) trigger a snapshot. |
| 120 | + # Patch and minor releases update the main docs in-place via the deploy job. |
| 121 | + snapshot: |
| 122 | + name: Snapshot versioned docs |
| 123 | + if: startsWith(github.ref, 'refs/tags/') |
| 124 | + runs-on: ubuntu-latest |
| 125 | + permissions: |
| 126 | + contents: write |
| 127 | + |
| 128 | + steps: |
| 129 | + - name: Checkout |
| 130 | + uses: actions/checkout@v4.2.2 |
| 131 | + with: |
| 132 | + # Full history needed to push to gh-pages and commit versions.json to main. |
| 133 | + fetch-depth: 0 |
| 134 | + |
| 135 | + - name: Extract major version from tag |
| 136 | + id: ver |
| 137 | + run: | |
| 138 | + # Validate that the tag strictly matches vX.0.0 before proceeding. |
| 139 | + # The workflow trigger filter (v[0-9]*.0.0) is the primary guard, but |
| 140 | + # this ensures the script fails fast if triggered with an unexpected ref. |
| 141 | + if ! echo "$GITHUB_REF_NAME" | grep -Eq '^v[0-9]+\.0\.0$'; then |
| 142 | + echo "Error: '$GITHUB_REF_NAME' does not match expected pattern vX.0.0" >&2 |
| 143 | + exit 1 |
| 144 | + fi |
| 145 | + MAJOR="${GITHUB_REF_NAME%%.*}" # e.g. v2 from v2.0.0 |
| 146 | + echo "major=$MAJOR" >> "$GITHUB_OUTPUT" |
| 147 | +
|
| 148 | + - name: Setup Bun |
| 149 | + uses: oven-sh/setup-bun@v2.1.2 |
| 150 | + with: |
| 151 | + bun-version: latest |
| 152 | + |
| 153 | + - name: Install dependencies |
| 154 | + run: bun install --frozen-lockfile |
| 155 | + |
| 156 | + - name: Build versioned snapshot |
| 157 | + env: |
| 158 | + # config.mts reads VITEPRESS_BASE when set; falls back to /github-code-search/ |
| 159 | + VITEPRESS_BASE: /github-code-search/${{ steps.ver.outputs.major }}/ |
| 160 | + run: bun run docs:build |
| 161 | + |
| 162 | + - name: Store snapshot in gh-pages branch |
| 163 | + run: | |
| 164 | + set -euo pipefail |
| 165 | + MAJOR="${{ steps.ver.outputs.major }}" |
| 166 | + git config user.name "github-actions[bot]" |
| 167 | + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" |
| 168 | + # Set up the gh-pages worktree (create orphan branch if it doesn't exist yet). |
| 169 | + if git ls-remote --heads origin gh-pages | grep -q gh-pages; then |
| 170 | + git fetch origin gh-pages |
| 171 | + git worktree add /tmp/gh-pages-storage origin/gh-pages |
| 172 | + else |
| 173 | + git worktree add --orphan -b gh-pages /tmp/gh-pages-storage |
| 174 | + touch /tmp/gh-pages-storage/.nojekyll # prevent Jekyll processing |
| 175 | + fi |
| 176 | + # Copy the built snapshot into its versioned directory. |
| 177 | + rm -rf "/tmp/gh-pages-storage/$MAJOR" |
| 178 | + cp -r docs/.vitepress/dist "/tmp/gh-pages-storage/$MAJOR" |
| 179 | + # Commit and push. |
| 180 | + cd /tmp/gh-pages-storage |
| 181 | + git add . |
| 182 | + git diff --staged --quiet || git commit -m "docs: store snapshot $MAJOR [skip ci]" |
| 183 | + git push origin gh-pages |
| 184 | + # Explicit cleanup — belt-and-suspenders even on ephemeral runners. |
| 185 | + git worktree remove /tmp/gh-pages-storage |
| 186 | +
|
| 187 | + - name: Prepend new version entry to versions.json |
| 188 | + run: | |
| 189 | + MAJOR="${{ steps.ver.outputs.major }}" |
| 190 | + LINK="/${MAJOR}/" |
| 191 | + # Idempotent — skip if the entry already exists. |
| 192 | + jq --arg text "$MAJOR" --arg link "$LINK" \ |
| 193 | + 'if any(.[]; .link == $link) then . else [{"text": $text, "link": $link}] + . end' \ |
| 194 | + docs/public/versions.json > /tmp/versions_new.json |
| 195 | + mv /tmp/versions_new.json docs/public/versions.json |
| 196 | +
|
| 197 | + - name: Commit versions.json to main |
| 198 | + uses: stefanzweifel/git-auto-commit-action@v5.0.1 |
| 199 | + with: |
| 200 | + # No [skip ci] — the push to main matches paths: docs/** and re-triggers |
| 201 | + # the deploy job, which merges the new snapshot into the Pages artifact. |
| 202 | + commit_message: "docs: add ${{ steps.ver.outputs.major }} to versions.json" |
| 203 | + file_pattern: docs/public/versions.json |
| 204 | + branch: main |
0 commit comments