diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4dd32b297..5b4860c6e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -104,6 +104,22 @@ The documentation site uses [Astro](https://astro.build/) with [Starlight](https This architecture keeps source docs clean and GitHub-friendly while providing a polished documentation site. +### Versioned Documentation Deployment + +The documentation site supports multiple versions simultaneously. When working with releases: + +- **Stable releases** (e.g., `v0.1.0`, `v0.2.0`) automatically deploy versioned docs +- **Documentation updates** can be made to old versions via `docs/v*` branches +- Users can switch between versions via the version switcher in the header + +For detailed information on the versioned documentation system, including: +- How version deployment works (tags vs branches) +- Releasing new versions +- Updating existing version docs +- Managing the version list + +See [docs/VERSIONED_DOCS.md](../docs/VERSIONED_DOCS.md). + ### Writing Documentation 1. **Create a markdown file** in the appropriate directory: diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4a155dd89..92ccf605d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,8 +2,11 @@ name: Documentation on: push: + tags: + - 'v*' branches: - main + - 'docs/v*' paths: - 'docs/**' - 'docs-site/**' @@ -19,9 +22,16 @@ on: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: - contents: read - pages: write - id-token: write + contents: write + +env: + # Site configuration - customize these for forks + # Production (dfinity/icp-cli): + PUBLIC_SITE: https://dfinity.github.io + PUBLIC_BASE_PREFIX: /icp-cli + # Forks should update these values: + # PUBLIC_SITE: https://your-username.github.io + # PUBLIC_BASE_PREFIX: /your-repo-name # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. @@ -30,10 +40,24 @@ concurrency: cancel-in-progress: false jobs: - # Build job + # Build job - validates that docs build successfully build: runs-on: ubuntu-latest steps: + - name: Validate required environment variables + run: | + if [[ -z "${{ env.PUBLIC_SITE }}" ]]; then + echo "❌ Error: PUBLIC_SITE environment variable is not set" + exit 1 + fi + if [[ -z "${{ env.PUBLIC_BASE_PREFIX }}" ]]; then + echo "❌ Error: PUBLIC_BASE_PREFIX environment variable is not set" + exit 1 + fi + echo "✅ Required environment variables are set:" + echo " PUBLIC_SITE=${{ env.PUBLIC_SITE }}" + echo " PUBLIC_BASE_PREFIX=${{ env.PUBLIC_BASE_PREFIX }}" + - name: Checkout uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 @@ -53,21 +77,154 @@ jobs: run: npm run build env: NODE_ENV: production + PUBLIC_SITE: ${{ env.PUBLIC_SITE }} + PUBLIC_BASE_PREFIX: ${{ env.PUBLIC_BASE_PREFIX }} + + # Publish root index and versions list - only runs on main branch + publish-root-files: + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - name: Upload artifact - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + - name: Setup Node + uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v4.2.0 with: - path: ./docs-site/dist + node-version: '20' + + - name: Prepare root files + run: | + mkdir -p root + + # Copy versions.json from repo + cp docs-site/versions.json root/versions.json + + # Extract the latest version from versions.json + LATEST_VERSION=$(jq -r ".versions[] | select(.latest == true) | .version" docs-site/versions.json) + + # If no releases yet, redirect to main branch docs + if [[ -z "$LATEST_VERSION" ]]; then + echo "⚠️ No releases yet, redirecting to main branch docs" + LATEST_VERSION="main" + else + echo "✅ Redirecting to version: ${LATEST_VERSION}" + fi + + # Generate index.html that redirects to latest version + cat > root/index.html << EOF + + +
+ + + + + EOF - # Deployment job - only runs on main branch - deploy: + - name: Deploy root files to GitHub Pages + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./root + keep_files: true + + # Publish main branch docs for preview (always available at /main/) + publish-main-docs: if: github.ref == 'refs/heads/main' && github.event_name == 'push' - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} + needs: build runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Setup Node + uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v4.2.0 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: docs-site/package-lock.json + + - name: Install dependencies + working-directory: ./docs-site + run: npm ci + + - name: Build main branch docs + working-directory: ./docs-site + run: | + BASE_PREFIX="${PUBLIC_BASE_PREFIX:-/icp-cli}" + echo "Building with base: ${BASE_PREFIX}/main/" + npm run build + env: + NODE_ENV: production + PUBLIC_BASE_PATH: ${{ env.PUBLIC_BASE_PREFIX }}/main/ + PUBLIC_SITE: ${{ env.PUBLIC_SITE }} + + - name: Deploy main docs to GitHub Pages + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs-site/dist + destination_dir: main + keep_files: true + + # Publish versioned docs - runs on tags (v*) or docs branches (docs/v*) + publish-versioned-docs: + if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/docs/v')) needs: build + runs-on: ubuntu-latest steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 + - name: Checkout + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Setup Node + uses: actions/setup-node@1a4442cacd436585916779262731d5b162bc6ec7 # v4.2.0 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: docs-site/package-lock.json + + - name: Install dependencies + working-directory: ./docs-site + run: npm ci + + - name: Extract version from tag or branch + run: | + # Use repo-specific base prefix from env vars (defaults to /icp-cli) + BASE_PREFIX="${PUBLIC_BASE_PREFIX:-/icp-cli}" + + if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then + # Tag: v0.1.0 -> extract major.minor -> 0.1 + VERSION=${GITHUB_REF#refs/tags/v} + # Strip patch version (0.1.0 -> 0.1) + VERSION=${VERSION%.*} + echo "DOCS_VERSION=${VERSION}" >> $GITHUB_ENV + echo "DOCS_BASE_PATH=${BASE_PREFIX}/${VERSION}/" >> $GITHUB_ENV + elif [[ "${GITHUB_REF}" == refs/heads/docs/v* ]]; then + # Branch: docs/v0.1 -> extract version -> 0.1 + VERSION=${GITHUB_REF#refs/heads/docs/v} + echo "DOCS_VERSION=${VERSION}" >> $GITHUB_ENV + echo "DOCS_BASE_PATH=${BASE_PREFIX}/${VERSION}/" >> $GITHUB_ENV + else + echo "❌ Docs should only be published for version tags (v*) or docs branches (docs/v*)" + echo "Current ref: ${GITHUB_REF}" + exit 1 + fi + + echo "Building docs for version ${VERSION} with base: ${BASE_PREFIX}/${VERSION}/" + + - name: Build documentation site + working-directory: ./docs-site + run: npm run build + env: + NODE_ENV: production + PUBLIC_BASE_PATH: ${{ env.DOCS_BASE_PATH }} + PUBLIC_SITE: ${{ env.PUBLIC_SITE }} + + - name: Deploy versioned docs to GitHub Pages + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs-site/dist + destination_dir: ${{ env.DOCS_VERSION }} diff --git a/.gitignore b/.gitignore index 000f39477..a28375559 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,6 @@ icp-cli-network-launcher # Documentation build artifacts docs-site/.astro/ docs-site/dist/ +docs-site/dist-test/ docs-site/node_modules/ docs-site/.docs-temp/ diff --git a/docs-site/astro.config.mjs b/docs-site/astro.config.mjs index 43e081947..e14c36c24 100644 --- a/docs-site/astro.config.mjs +++ b/docs-site/astro.config.mjs @@ -4,8 +4,11 @@ import rehypeExternalLinks from 'rehype-external-links'; // https://astro.build/config export default defineConfig({ - site: 'https://dfinity.github.io', - base: process.env.NODE_ENV === 'production' ? '/icp-cli/' : '/', + site: process.env.PUBLIC_SITE, + // For versioned deployments: /icp-cli/0.1/, /icp-cli/0.2/, etc. + // For non-versioned: /icp-cli/ in production, / in development + // Defaults are set in the workflow, not here + base: process.env.PUBLIC_BASE_PATH || (process.env.NODE_ENV === 'production' ? process.env.PUBLIC_BASE_PREFIX + '/' : '/'), markdown: { rehypePlugins: [ // Open external links in new tab @@ -17,6 +20,9 @@ export default defineConfig({ title: 'ICP CLI', description: 'Command-line tool for developing and deploying applications on the Internet Computer Protocol (ICP)', favicon: '/favicon.png', + components: { + SiteTitle: './src/components/SiteTitle.astro', + }, head: [ { tag: 'script', @@ -38,6 +44,7 @@ export default defineConfig({ logo: { src: './src/assets/icp-logo.svg', replacesTitle: false, + alt: 'ICP', }, customCss: [ './src/styles/layers.css', diff --git a/docs-site/src/components/SiteTitle.astro b/docs-site/src/components/SiteTitle.astro new file mode 100644 index 000000000..f91812ddb --- /dev/null +++ b/docs-site/src/components/SiteTitle.astro @@ -0,0 +1,73 @@ +--- +import type { Props } from '@astrojs/starlight/props'; +import config from 'virtual:starlight/user-config'; +import VersionSwitcher from './VersionSwitcher.astro'; +import icpLogo from '../assets/icp-logo.svg'; + +// Ensure title is a string +const titleText = typeof config.title === 'string' ? config.title : 'ICP CLI'; + +// Extract logo info safely, but use imported asset for src +const logo = config.logo as any; +const hasLightLogo = logo && 'src' in logo; +const hasDarkLogo = logo && 'dark' in logo && typeof logo.dark === 'object' && logo.dark && 'src' in logo.dark; + +// Use imported logo asset instead of config path +const logoSrc = icpLogo.src; +--- + +Redirecting to latest version...
+ + +EOF +echo "✓ index.html created" + +echo "" +echo "==================================================" +echo "✓ All versions built successfully!" +echo "==================================================" +echo "" + +# Verify the structure +echo "Verifying test structure..." +echo "Directory: $TEST_DIR$BASE_PREFIX/" +ls -lah "$TEST_DIR$BASE_PREFIX/" +echo "" +echo "Checking version subdirectories..." +for version in 0.1 0.2 main; do + echo "" + if [ -d "$TEST_DIR$BASE_PREFIX/$version" ]; then + echo "✓ $version/ exists" + echo " Files: $(ls "$TEST_DIR$BASE_PREFIX/$version" | wc -l)" + + if [ -f "$TEST_DIR$BASE_PREFIX/$version/index.html" ]; then + echo " index.html: ✓" + + # Check BASE_URL in the built HTML + BASE_URL_IN_HTML=$(grep -o 'import\.meta\.env\.BASE_URL[^"]*"[^"]*"' "$TEST_DIR$BASE_PREFIX/$version/index.html" | head -1 || echo "not found") + echo " BASE_URL in HTML: $BASE_URL_IN_HTML" + + # Check if version switcher is present + if grep -q "version-switcher" "$TEST_DIR$BASE_PREFIX/$version/index.html"; then + echo " VersionSwitcher: ✓ present" + + # Check what's rendered (dev/main badge or button) + if grep -q "version-button" "$TEST_DIR$BASE_PREFIX/$version/index.html"; then + echo " Renders: version button (interactive dropdown)" + elif grep -q ">main<" "$TEST_DIR$BASE_PREFIX/$version/index.html"; then + echo " Renders: 'main' badge (⚠️ unexpected for $version)" + elif grep -q ">dev<" "$TEST_DIR$BASE_PREFIX/$version/index.html"; then + echo " Renders: 'dev' badge (⚠️ unexpected)" + else + echo " Renders: unknown" + fi + else + echo " VersionSwitcher: ✗ not found" + fi + else + echo " index.html: ✗ MISSING" + fi + + # Check for assets directory + if [ -d "$TEST_DIR$BASE_PREFIX/$version/_astro" ]; then + echo " _astro/ assets: ✓ ($(ls "$TEST_DIR$BASE_PREFIX/$version/_astro" | wc -l) files)" + else + echo " _astro/ assets: ✗ MISSING" + fi + else + echo "✗ $version/ MISSING" + fi +done +echo "" + +# Start Python HTTP server (most reliable for static files) +if ! command -v python3 &> /dev/null; then + echo "⚠️ Warning: python3 not found. Cannot start server." + echo "Files are built in: $TEST_DIR$BASE_PREFIX/" +else + echo "Starting local server with Python..." + echo "Server starting at: http://localhost:${TEST_PORT}" + echo "" + echo "Press Ctrl+C to stop the server when done testing" + echo "" + echo "Test URLs:" + echo " - http://localhost:${TEST_PORT}$BASE_PREFIX/ (should redirect to 0.2)" + echo " - http://localhost:${TEST_PORT}$BASE_PREFIX/0.2/ (version 0.2)" + echo " - http://localhost:${TEST_PORT}$BASE_PREFIX/0.1/ (version 0.1)" + echo " - http://localhost:${TEST_PORT}$BASE_PREFIX/main/ (main branch)" + echo "" + echo "Expected behavior:" + echo " ✓ Version 0.2: Button shows 'v0.2', dropdown shows both versions" + echo " ✓ Version 0.1: Button shows 'v0.1', dropdown shows both versions" + echo " ✓ Main: Shows 'main' badge (no dropdown)" + echo " ✓ Clicking versions navigates between them" + echo " ✓ Console shows [VersionSwitcher] logs" + echo "" + echo "If you see 404s or wrong versions:" + echo " 1. Check the structure output above" + echo " 2. Check browser DevTools Console for BASE_URL" + echo " 3. Check browser DevTools Network tab for asset paths" + echo "" + echo "Starting server in 3 seconds..." + sleep 3 + + cd "$TEST_DIR" + python3 -m http.server ${TEST_PORT} & + SERVER_PID=$! + + echo "" + echo "Server is running (PID: $SERVER_PID)" + echo "Press Ctrl+C to stop the server and exit" + echo "" + + # Wait for server process + wait "$SERVER_PID" +fi diff --git a/docs-site/versions.json b/docs-site/versions.json new file mode 100644 index 000000000..d28705ba6 --- /dev/null +++ b/docs-site/versions.json @@ -0,0 +1,4 @@ +{ + "$comment": "Before first release: Empty array redirects to /main/. After releases: Add versions here (newest first). Only 'version' is required; 'latest: true' marks the current release.", + "versions": [] +} diff --git a/docs/VERSIONED_DOCS.md b/docs/VERSIONED_DOCS.md new file mode 100644 index 000000000..7fc4e9f3b --- /dev/null +++ b/docs/VERSIONED_DOCS.md @@ -0,0 +1,167 @@ +# Versioned Documentation Setup + +This document explains how the versioned documentation system works for the ICP CLI. + +## Overview + +The documentation site supports multiple versions simultaneously: +- `https://dfinity.github.io/icp-cli/` → Redirects to latest version +- `https://dfinity.github.io/icp-cli/0.1/` → Version 0.1 docs +- `https://dfinity.github.io/icp-cli/main/` → Main branch docs (preview) + +Users can switch between versions using the version switcher dropdown in the header. + +## Architecture + +### Directory Structure (gh-pages branch) + +``` +├── index.html # Redirects to latest version (or /main/ if no releases) +├── versions.json # List of available versions +├── main/ # Main branch docs (always updated) +├── 0.1/ # Version 0.1 docs +├── 0.2/ # Version 0.2 docs +└── ... +``` + +### Workflow Triggers + +The workflow [`.github/workflows/docs.yml`](.github/workflows/docs.yml) handles deployment: + +| Trigger | Action | +|---------|--------| +| Tag `v*` (e.g., `v0.1.0`) | Deploys to `/0.1/` (major.minor) | +| Branch `docs/v*` (e.g., `docs/v0.1`) | Updates `/0.1/` (for cherry-picks) | +| Push to `main` | Deploys to `/main/`, updates root `index.html` and `versions.json` | + +### Version Switcher + +The component ([VersionSwitcher.astro](../docs-site/src/components/VersionSwitcher.astro)): +- Extracts current version from the URL path at build time +- Fetches `versions.json` at runtime using the configured base prefix +- Shows "dev" badge in local development, "main" badge on main branch +- Shows interactive dropdown with all versions for released docs + +## Configuration + +### Environment Variables + +Set these in the workflow file to configure deployment: + +```yaml +env: + PUBLIC_SITE: https://dfinity.github.io # GitHub Pages base URL + PUBLIC_BASE_PREFIX: /icp-cli # Repository path prefix +``` + +**For forks**, update both values: +```yaml +env: + PUBLIC_SITE: https://your-username.github.io + PUBLIC_BASE_PREFIX: /your-repo-name +``` + +The `build` job validates these are set before proceeding. + +### GitHub Pages Settings + +In **Settings → Pages**: +- **Source**: Deploy from a branch +- **Branch**: `gh-pages` / `/ (root)` + +### versions.json + +Located at [docs-site/versions.json](../docs-site/versions.json). Update when releasing: + +```json +{ + "versions": [ + {"version": "0.2", "latest": true}, + {"version": "0.1"} + ] +} +``` + +The workflow copies this to gh-pages root and generates `index.html` redirecting to the first entry. + +## Common Tasks + +### First Deployment (Pre-release) + +```bash +git push origin main +# → Deploys to /main/, redirect points to /main/ +``` + +### First Release + +```bash +# 1. Deploy docs +git tag v0.1.0 +git push origin v0.1.0 + +# 2. Update versions.json: add {"version": "0.1", "latest": true} +git add docs-site/versions.json +git commit -m "docs: add v0.1 to version list" +git push origin main +``` + +### Subsequent Releases + +```bash +# 1. Deploy docs +git tag v0.2.0 +git push origin v0.2.0 + +# 2. Update versions.json: add 0.2 at top with latest: true, remove latest from 0.1 +git add docs-site/versions.json +git commit -m "docs: add v0.2 to version list" +git push origin main +``` + +**Important**: Push the tag first, then update versions.json to avoid 404s. + +### Update Old Version Docs + +```bash +git checkout v0.1.0 +git checkout -b docs/v0.1 +# Make changes +git commit -m "docs: fix typo in v0.1" +git push origin docs/v0.1 +``` + +Or push a patch tag (`v0.1.1`) — it deploys to the same `/0.1/` directory. + +### Beta Versions + +Create a docs branch with the full version: + +```bash +git checkout -b docs/v0.2.0-beta.5 +git push origin docs/v0.2.0-beta.5 +# → Deploys to /0.2.0-beta.5/ +``` + +Don't add beta versions to `versions.json` — they won't appear in the switcher. + +## Local Testing + +Run the test script to simulate the full deployment locally: + +```bash +./docs-site/test-version-switcher.sh +``` + +This builds multiple versions and serves them at `http://localhost:4321/icp-cli/`. + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| Workflow fails with "environment variable is not set" | Add `PUBLIC_SITE` and `PUBLIC_BASE_PREFIX` to workflow `env:` section | +| Version switcher shows "Failed to load versions" | Check `versions.json` exists at gh-pages root, check browser console | +| Tag pushed but docs not deployed | Verify tag matches `v*` pattern, check workflow logs | +| New version not in switcher | Push `versions.json` update to main after adding the version | +| Redirect not working | Check `index.html` in gh-pages, clear browser cache | +| Deployment replaces other versions | Verify `keep_files: true` and correct `destination_dir` in workflow | diff --git a/scripts/prepare-docs.sh b/scripts/prepare-docs.sh index 58b9cf266..91b4eed04 100755 --- a/scripts/prepare-docs.sh +++ b/scripts/prepare-docs.sh @@ -42,7 +42,7 @@ echo "" echo "Step 1: Copying documentation files..." rm -rf "$TARGET_DIR" mkdir -p "$TARGET_DIR" -rsync -a --exclude='schemas/' --exclude='README.md' --exclude='*/README.md' "$SOURCE_DIR/" "$TARGET_DIR/" +rsync -a --exclude='schemas/' --exclude='README.md' --exclude='*/README.md' --exclude='VERSIONED_DOCS.md' "$SOURCE_DIR/" "$TARGET_DIR/" echo "✓ Files copied" # Step 2: Fix markdown links for Starlight's directory structure @@ -109,9 +109,9 @@ find "$TARGET_DIR" -name "*.md" -type f | while read -r file; do { echo "---" echo "title: $title" - # Add banner to all pages (will be removed once versioning is introduced) + # Banner encouraging user feedback echo "banner:" - echo " content: 'This documentation reflects the latest main branch and may include features not yet in the current beta release. Feedback welcome on the Forum or Discord!'" + echo " content: 'Feedback welcome! Report issues on GitHub, ask questions on the Forum, or chat with us on Discord.'" echo "---" echo "" # Remove the first H1 heading line from content to avoid duplicates