diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml index a1b8597147..fb79f04128 100644 --- a/.github/workflows/update.yml +++ b/.github/workflows/update.yml @@ -2,6 +2,7 @@ name: Update Plugin Index on: workflow_dispatch: + pull_request: schedule: # "every 2 hours" - cron: 0 */2 * * * @@ -45,7 +46,7 @@ jobs: run: | python scripts/fetch_manifests.py - # validate and re-index plugin versions in case new ones have been + # validate and re-index plugin versions in case new ones have been # retrieved from bigquery # will update extended_summary.json, index.json and pypi.json # to include all plugin versions @@ -58,7 +59,33 @@ jobs: # push changes back to the repo, this will trigger a new vercel build # https://github.com/stefanzweifel/git-auto-commit-action + # skip on PRs - we only want to validate, not modify the PR - uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0 + if: github.event_name != 'pull_request' with: commit_message: '[bot] Update Plugin Index' commit_author: github-actions[bot] + + # prepare static files for gh-pages deployment + # do this AFTER the vercel commit to make sure we're not messing with anything + - name: Prepare gh-pages + run: | + python scripts/prepare_gh_pages.py + + # upload gh-pages artifact for PRs (for inspection) + - name: Upload gh-pages artifact + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + with: + name: gh-pages-preview + path: ./gh-pages + + # deploy to gh-pages branch (only on main/scheduled runs, not PRs) + - name: Deploy to GitHub Pages + if: github.event_name != 'pull_request' + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./gh-pages + publish_branch: gh-pages + force_orphan: true diff --git a/.gitignore b/.gitignore index ab59128d8a..3724fb2403 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ # production /build +/gh-pages # misc .DS_Store diff --git a/pages/api/conda.js b/pages/api/conda-map.js similarity index 100% rename from pages/api/conda.js rename to pages/api/conda-map.js diff --git a/scripts/prepare_gh_pages.py b/scripts/prepare_gh_pages.py new file mode 100644 index 0000000000..8ae52e951c --- /dev/null +++ b/scripts/prepare_gh_pages.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +"""Prepare static files for GitHub Pages deployment. + +This script creates a gh-pages directory with the same structure as public/ +but formatted for static file serving: +- No .json file extensions (to match API routes) +- conda.json -> conda-map (to avoid /conda conflicts with /conda/{plugin_name}) +- Everything nested under an 'api' directory +- Simple index.html at the root +""" + +import json +import shutil +from pathlib import Path + + +def prepare_gh_pages(): + """Generate gh-pages directory structure from public directory.""" + root_dir = Path(__file__).parent.parent + public_dir = root_dir / "public" + gh_pages_dir = root_dir / "gh-pages" + api_dir = gh_pages_dir / "api" + + if gh_pages_dir.exists(): + shutil.rmtree(gh_pages_dir) + gh_pages_dir.mkdir(exist_ok=True) + api_dir.mkdir(exist_ok=True) + + print(f"📁 Preparing gh-pages directory at {gh_pages_dir}") + + file_count = 0 + + for source_file in public_dir.rglob("*"): + if not source_file.is_file(): + continue + + if source_file.parent == "github": + # TODO: replace when github fetching is fixed + continue + + rel_path = source_file.relative_to(public_dir) + dest_paths = [] + + # special cases for some json files avaialable *outside* the API route + if source_file == public_dir / "classifiers.json": + dest_paths.append(gh_pages_dir / "classifiers.json") + elif source_file == public_dir / "conda.json": + dest_paths.append(gh_pages_dir / "conda.json") + dest_paths.append(api_dir / "conda-map") + elif source_file == public_dir / "errors.json": + dest_paths.append(gh_pages_dir / "errors.json") + elif source_file == public_dir / "extended_summary.json": + dest_paths.append(gh_pages_dir / "extended_summary.json") + dest_paths.append(api_dir / "extended_summary") + elif source_file == public_dir / "index.json": + dest_paths.append(gh_pages_dir / "index.json") + dest_paths.append(api_dir / "plugins") + elif source_file == public_dir / "readers.json": + dest_paths.append(gh_pages_dir / "readers.json") + else: + dest_paths.append(api_dir / str(rel_path).removesuffix(".json")) + + for dest_path in dest_paths: + dest_path.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source_file, dest_path) + file_count += 1 + + if file_count % 100 == 0: + print(f" Copied {file_count} files...") + + print(f"✅ Copied {file_count} files to gh-pages/api/") + + create_index_html(gh_pages_dir, api_dir) + + print("🎉 gh-pages directory ready for deployment!") + + +def create_index_html(gh_pages_dir: Path, api_dir: Path): + """Create a simple index.html that mimics the Next.js homepage.""" + + # get the list of all plugins + index_file = api_dir / "plugins" + with open(index_file) as f: + index_data = json.load(f) + + plugin_list_html = "\n".join( + [ + f'
  • {name}-{version}
  • ' + for name, version in sorted(index_data.items()) + ] + ) + + html_content = f""" + + + + + napari plugins + + +
    +

    napari plugins

    +

    This page serves the REST API for napari plugins.

    +

    + It is generated by the source code in the + npe2api repo. +

    +

    + A user-friendly site for napari plugin information is + napari-hub.org +

    +

    Commonly used API endpoints

    +

    Plugin index: api/plugins

    +

    + Summary info: + api/extended_summary +

    +

    + PyPI information: + Use api/pypi/pypi_plugin_name + endpoint for an individual plugin. +

    +

    + Conda index: api/conda-map + (see conda info for each plugin at api/conda/pypi_name) +

    +

    Fetch errors: errors.json

    +

    Plugin manifests

    + +
    + + +""" + + index_html = gh_pages_dir / "index.html" + with open(index_html, "w") as f: + f.write(html_content) + + print(f"📄 Created index.html with {len(index_data)} plugins listed") + + +if __name__ == "__main__": + prepare_gh_pages() diff --git a/vercel.json b/vercel.json index 75e0e4bf7c..e7ff61dfc9 100644 --- a/vercel.json +++ b/vercel.json @@ -9,6 +9,29 @@ } ], "destination": "https://api.napari.org/api/:path*" + }, + { + "source": "/api/conda", + "destination": "/api/conda-map" + } + ], + "headers": [ + { + "source": "/api/conda", + "headers": [ + { + "key": "Deprecation", + "value": "true" + }, + { + "key": "Link", + "value": "; rel=\"alternate\"" + }, + { + "key": "X-Deprecated-Message", + "value": "This endpoint is deprecated. Use /api/conda-map instead to avoid conflicts with /api/conda/:plugin routes." + } + ] } ] } \ No newline at end of file