Skip to content

Merge pull request #90 from fulll/feat/promotion-polish #1

Merge pull request #90 from fulll/feat/promotion-polish

Merge pull request #90 from fulll/feat/promotion-polish #1

Workflow file for this run

name: Docs
# Triggers:
# - push to main touching docs/** or this workflow → deploy latest to GitHub Pages
# - push of a major release tag (v2.0.0, v3.0.0 …) → versioned snapshot
# - workflow_dispatch → manual deploy of latest
on:
push:
branches: [main]
paths:
- "docs/**"
- ".github/workflows/docs.yaml"
tags:
# Glob pattern (not regex): matches v1.0.0, v2.0.0, v10.0.0 …
- "v[0-9]*.0.0"
workflow_dispatch:
# Deployment strategy: GitHub Actions Pages source (actions/deploy-pages — official, no third party).
# • gh-pages branch = versioned snapshot STORAGE only (not served directly by Pages)
# • Every deploy job assembles a combined artifact:
# - latest docs built from main at the artifact root
# - each vX/ snapshot from the gh-pages branch merged in
# → single artifact deployed via actions/deploy-pages
# • Snapshot job stores the built snapshot in gh-pages branch via plain git,
# then pushes the updated versions.json to main (no [skip ci]) which
# re-triggers the deploy job to include the new snapshot immediately.
# Requires: Settings → Pages → Source: GitHub Actions.
permissions:
contents: write
pages: write
id-token: write
# Only one concurrent deployment for the same ref; cancel outdated runs.
concurrency:
group: docs-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Deploy (latest) ─────────────────────────────────────────────────────────
# Assembles a combined Pages artifact:
# 1. Builds the latest docs (base: /github-code-search/)
# 2. Merges existing versioned snapshots from the gh-pages storage branch
# into the artifact (docs/.vitepress/dist/vX/ for each stored version)
# 3. Uploads the artifact and deploys via actions/deploy-pages
# Requires: Settings → Pages → Source: GitHub Actions.
deploy:
name: Build and deploy docs
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Needed to fetch the gh-pages storage branch for versioned snapshots.
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build latest docs
run: bun run docs:build
# Base URL: /github-code-search/ (default in config.mts)
- name: Merge versioned snapshots from gh-pages storage
run: |
set -euo pipefail
if git ls-remote --heads origin gh-pages | grep -q gh-pages; then
git fetch origin gh-pages
# List only top-level entries that match v<integer> (e.g. v1, v2).
# Using a pathspec glob and a strict regex avoids iterating over
# unrelated files (.nojekyll, README, etc.) at the branch root.
for entry in $(git ls-tree --name-only origin/gh-pages -- 'v[0-9]*'); do
if echo "$entry" | grep -qE '^v[0-9]+$'; then
echo "Merging snapshot: $entry"
mkdir -p "docs/.vitepress/dist/$entry"
git archive origin/gh-pages "$entry" | tar -x -C docs/.vitepress/dist/
# Validate: warn and remove if the extracted directory is empty.
if [ -z "$(ls -A "docs/.vitepress/dist/$entry" 2>/dev/null)" ]; then
echo "Warning: snapshot '$entry' is empty after extraction — removing."
rmdir "docs/.vitepress/dist/$entry"
fi
fi
done
else
echo "No gh-pages branch yet — skipping snapshot merge"
fi
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
with:
path: docs/.vitepress/dist
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
# ── Snapshot (versioned) ────────────────────────────────────────────────────
# Triggered by a major release tag (e.g. v2.0.0).
# 1. Builds docs with a versioned base URL (/github-code-search/v2/).
# 2. Stores the output in the gh-pages branch under /v2/ using plain git
# (no third-party action). The gh-pages branch is storage only — Pages
# still points to GitHub Actions; the deploy job merges snapshots in.
# 3. Prepends the entry to versions.json on main WITHOUT [skip ci], which
# re-triggers the deploy job so the new snapshot is live immediately.
#
# Convention: only tags matching vX.0.0 (major bumps) trigger a snapshot.
# Patch and minor releases update the main docs in-place via the deploy job.
snapshot:
name: Snapshot versioned docs
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Full history needed to push to gh-pages and commit versions.json to main.
fetch-depth: 0
- name: Extract major version from tag
id: ver
run: |
# Validate that the tag strictly matches vX.0.0 before proceeding.
# The workflow trigger filter (v[0-9]*.0.0) is the primary guard, but
# this ensures the script fails fast if triggered with an unexpected ref.
if ! echo "$GITHUB_REF_NAME" | grep -Eq '^v[0-9]+\.0\.0$'; then
echo "Error: '$GITHUB_REF_NAME' does not match expected pattern vX.0.0" >&2
exit 1
fi
MAJOR="${GITHUB_REF_NAME%%.*}" # e.g. v2 from v2.0.0
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
- name: Setup Bun
uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2.1.3
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Build versioned snapshot
env:
# config.mts reads VITEPRESS_BASE when set; falls back to /github-code-search/
VITEPRESS_BASE: /github-code-search/${{ steps.ver.outputs.major }}/
run: bun run docs:build
- name: Store snapshot in gh-pages branch
run: |
set -euo pipefail
MAJOR="${{ steps.ver.outputs.major }}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Set up the gh-pages worktree (create orphan branch if it doesn't exist yet).
if git ls-remote --heads origin gh-pages | grep -q gh-pages; then
git fetch origin gh-pages
git worktree add /tmp/gh-pages-storage origin/gh-pages
else
git worktree add --orphan -b gh-pages /tmp/gh-pages-storage
touch /tmp/gh-pages-storage/.nojekyll # prevent Jekyll processing
fi
# Copy the built snapshot into its versioned directory.
rm -rf "/tmp/gh-pages-storage/$MAJOR"
cp -r docs/.vitepress/dist "/tmp/gh-pages-storage/$MAJOR"
# Commit and push.
cd /tmp/gh-pages-storage
git add .
git diff --staged --quiet || git commit -m "docs: store snapshot $MAJOR [skip ci]"
git push origin gh-pages
# Explicit cleanup — belt-and-suspenders even on ephemeral runners.
git worktree remove /tmp/gh-pages-storage
- name: Prepend new version entry to versions.json
run: |
MAJOR="${{ steps.ver.outputs.major }}"
LINK="/${MAJOR}/"
# Idempotent — skip if the entry already exists.
jq --arg text "$MAJOR" --arg link "$LINK" \
'if any(.[]; .link == $link) then . else [{"text": $text, "link": $link}] + . end' \
docs/public/versions.json > /tmp/versions_new.json
mv /tmp/versions_new.json docs/public/versions.json
- name: Generate blog post stub for new major version
run: |
set -euo pipefail
# Convert vX.0.0 → v<maj>-0-0 for the file name (e.g. v3.0.0 → v3-0-0)
TAG="$GITHUB_REF_NAME"
SLUG="${TAG//./-}" # v3.0.0 → v3-0-0
BLOG_FILE="docs/blog/release-${SLUG}.md"
RELEASE_DATE="$(date -u +%Y-%m-%d)"
# Idempotent — skip creation if the file already exists (manually authored).
if [ -f "$BLOG_FILE" ]; then
echo "Blog post $BLOG_FILE already exists — skipping stub generation."
else
# Fix: use Python to write the file so heredoc indentation never
# leaks into the generated Markdown (which would break frontmatter).
python3 - <<PY
import pathlib
tag = "$TAG"
release_date = "$RELEASE_DATE"
blog_file = "$BLOG_FILE"
content = (
f'---\ntitle: "What\'s new in {tag}"\n'
f'description: "Highlights of github-code-search {tag}"\n'
f'date: {release_date}\n---\n\n'
f'# What\'s new in github-code-search {tag}\n\n'
f'> Full release notes: <https://github.com/fulll/github-code-search/releases/tag/{tag}>\n\n'
'<!-- TODO: fill in feature highlights, usage examples and screenshots. -->\n'
)
pathlib.Path(blog_file).write_text(content, encoding="utf-8")
print(f"Created blog stub: {blog_file}")
PY
fi
- name: Update blog/index.md table with new major version
run: |
set -euo pipefail
TAG="$GITHUB_REF_NAME"
SLUG="${TAG//./-}"
# Add a row to the blog index table only if the version isn't already listed.
if grep -qF "release-${SLUG}" docs/blog/index.md; then
echo "Blog index already contains ${TAG} — skipping."
else
python3 - <<PY
import re, pathlib
path = pathlib.Path("docs/blog/index.md")
content = path.read_text()
tag = "$TAG"
slug = "$SLUG"
new_row = f"| [{tag}](./release-{slug}) | <!-- TODO: add summary --> |"
# Insert the new row after the last markdown table row (line ending with |).
updated = re.sub(
r"(\|[^\n]+\|)(\s*\Z)",
lambda m: m.group(1) + "\n" + new_row + m.group(2),
content,
count=1,
flags=re.DOTALL,
)
path.write_text(updated)
print(f"Inserted row for {tag} into blog/index.md")
PY
fi
- name: Commit blog stub and versions.json to main
# Pin to exact commit SHA to prevent supply-chain attacks.
uses: stefanzweifel/git-auto-commit-action@8621497c8c39c72f3e2a999a26b4ca1b5058a842 # v5.0.1
with:
# No [skip ci] — the push to main matches paths: docs/** and re-triggers
# the deploy job, which merges the new snapshot into the Pages artifact.
commit_message: "docs: add ${{ steps.ver.outputs.major }} to versions.json and blog"
file_pattern: docs/public/versions.json docs/blog/
branch: main