Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion .github/workflows/update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: Update Plugin Index

on:
workflow_dispatch:
pull_request:
schedule:
# "every 2 hours"
- cron: 0 */2 * * *
Expand Down Expand Up @@ -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
Expand All @@ -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] <github-actions[bot]@users.noreply.github.com>

# 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

# production
/build
/gh-pages

# misc
.DS_Store
Expand Down
File renamed without changes.
145 changes: 145 additions & 0 deletions scripts/prepare_gh_pages.py
Original file line number Diff line number Diff line change
@@ -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")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so I notice sometimes we append the path with no .json to dest_paths as well as with the .json suffix. Can you explain why that is? I guess api_dir is the pages that are actually going to be served, and gh_pages_dir is like... backup files? Why do we put them in there at all?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's really an attempt to provide some backward-compatibility, but perhaps we don't need it. I think it's really an accident that both https://api.napari.org/conda.json and https://api.napari.org/api/conda point to the same thing in the current implementation.

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' <li><a href="api/manifest/{name}">{name}-{version}</a></li>'
for name, version in sorted(index_data.items())
]
)

html_content = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>napari plugins</title>
</head>
<body>
<div>
<h1>napari plugins</h1>
<p>This page serves the REST API for napari plugins.</p>
<p>
It is generated by the source code in the
<a href="https://github.com/napari/npe2api">npe2api repo</a>.
</p>
<p>
A user-friendly site for napari plugin information is
<a href="https://napari-hub.org/">napari-hub.org</a>
</p>
<h3>Commonly used API endpoints</h3>
<p>Plugin index: <a href="api/plugins">api/plugins</a></p>
<p>
Summary info:
<a href="api/extended_summary">api/extended_summary</a>
</p>
<p>
PyPI information:
Use <code>api/pypi/pypi_plugin_name</code>
endpoint for an individual plugin.
</p>
<p>
Conda index: <a href="api/conda-map">api/conda-map</a>
(see conda info for each plugin at api/conda/pypi_name)
</p>
<p>Fetch errors: <a href="errors.json">errors.json</a></p>
<h3>Plugin manifests</h3>
<ul>
{plugin_list_html}
</ul>
</div>
</body>
</html>
"""

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()
23 changes: 23 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "</api/conda-map>; rel=\"alternate\""
},
{
"key": "X-Deprecated-Message",
"value": "This endpoint is deprecated. Use /api/conda-map instead to avoid conflicts with /api/conda/:plugin routes."
}
]
}
]
}
Loading