diff --git a/.github/actions/generate-metadata/action.yml b/.github/actions/generate-metadata/action.yml index eca2bd43..e4ab1631 100644 --- a/.github/actions/generate-metadata/action.yml +++ b/.github/actions/generate-metadata/action.yml @@ -67,7 +67,3 @@ runs: umask 000 && chmod +w ../aiida-registry aiida-registry test-install shell: bash - - - name: Move JSON file to the React project - run: cp plugins_metadata.json aiida-registry-app/src/ - shell: bash diff --git a/.github/scripts/diff_changed_plugins.py b/.github/scripts/diff_changed_plugins.py new file mode 100644 index 00000000..75b6d3c6 --- /dev/null +++ b/.github/scripts/diff_changed_plugins.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Detect plugins added or modified in this PR vs the base branch. + +Reads `plugins.yaml` at `origin/master` and at `HEAD`, prints the list of +plugin keys that were added or whose entry differs, and writes the same list +plus a count to GITHUB_OUTPUT for downstream jobs to consume. + +Plugin keys must match a conservative allow-list — anything else is dropped +with a warning so we don't pass adversarial strings into shell commands later. +""" + +import os +import re +import subprocess +import sys + +import yaml + +SAFE_KEY = re.compile(r"^[A-Za-z0-9._-]+$") + + +def load_yaml_at(ref): + proc = subprocess.run( + ["git", "show", f"{ref}:plugins.yaml"], + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + # plugins.yaml may not exist at the base ref (brand-new repo etc.). + print(f"::warning::Could not read plugins.yaml at {ref}: {proc.stderr.strip()}") + return {} + return yaml.safe_load(proc.stdout) or {} + + +def main(): + base = load_yaml_at("origin/master") + head = load_yaml_at("HEAD") + + unsafe = sorted(k for k in head if not SAFE_KEY.match(str(k))) + if unsafe: + print(f"::warning::Skipping plugin keys with unsafe characters: {unsafe}") + + changed = sorted( + key + for key, value in head.items() + if SAFE_KEY.match(str(key)) and (key not in base or base[key] != value) + ) + + output = os.environ.get("GITHUB_OUTPUT") + if output: + with open(output, "a", encoding="utf8") as fh: + fh.write(f"changed={' '.join(changed)}\n") + fh.write(f"changed_count={len(changed)}\n") + + print(f"Changed plugins ({len(changed)}): {' '.join(changed) or '(none)'}") + + +if __name__ == "__main__": + sys.exit(main() or 0) diff --git a/.github/scripts/extract_pr_findings.py b/.github/scripts/extract_pr_findings.py new file mode 100644 index 00000000..0031f99f --- /dev/null +++ b/.github/scripts/extract_pr_findings.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Render a PR comment summarising warnings/errors for changed plugins. + +Reads `plugins_metadata.json`, extracts the `warnings` and `errors` lists for +each plugin key passed on the command line, renders a markdown body with a +sticky-comment marker, and writes the body plus a `has_findings` flag to +GITHUB_OUTPUT. Exits non-zero when any of the listed plugins has at least one +warning or error so the workflow check goes red. + +Stages: + --stage warnings Comment only reflects W001-W020 (fetch step). + --stage install Comment additionally reflects E001-E004 (test-install). +""" + +import argparse +import json +import os +import re +import sys +from pathlib import Path + +COMMENT_MARKER = "" +README_LINK = ( + "https://github.com/aiidateam/aiida-registry" + "#how-to-fix-registry-warnings-and-errors" +) + + +def render_message(raw): + """Convert an HTML-tagged registry message into markdown.""" + pre_block = "" + pre_match = re.match(r"^(.*?)
(.*?)
(.*)$", raw, re.DOTALL) + if pre_match: + raw = pre_match.group(1) + pre_match.group(3) + pre_block = pre_match.group(2).strip() + + a_match = re.match( + r"^([WE]\d+):\s*(.*)$", + raw, + re.DOTALL, + ) + if a_match: + head = f"`{a_match.group(2)}`: {a_match.group(3).strip()}" + else: + head = re.sub(r"<[^>]+>", "", raw).strip() + + if pre_block: + # Keep error output to a reasonable size in PR comments. + if len(pre_block) > 2000: + pre_block = pre_block[:2000] + "\n... (truncated)" + head += f"\n\n```\n{pre_block}\n```" + return head + + +def write_outputs(body, has_findings): + output = os.environ.get("GITHUB_OUTPUT") + if output: + with open(output, "a", encoding="utf8") as fh: + fh.write(f"has_findings={'true' if has_findings else 'false'}\n") + fh.write("body< (c.body || "").includes(marker), + ); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + - name: Fail if findings + if: steps.extract.outputs.has_findings == 'true' + run: | + echo "::error::Warnings detected on changed plugin entries." + exit 1 + + install-check: + # NOTE: this job is gated by the `plugin-install-test` Environment, which + # MUST be configured in repo settings with required reviewers BEFORE this + # workflow can serve as a real gate. If the environment is missing GitHub + # auto-creates one with no protection rules and the gate is open. + needs: [get-pr, diff-changed, warnings-check] + if: needs.diff-changed.outputs.changed_count != '0' + runs-on: ubuntu-latest + environment: plugin-install-test + env: + CHANGED: ${{ needs.diff-changed.outputs.changed }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.get-pr.outputs.merge_commit_sha }} + + - uses: ./.github/actions/create-dev-env + + - name: Fetch metadata for changed plugins + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: aiida-registry fetch $CHANGED + + - name: Test-install changed plugins + run: aiida-registry test-install $CHANGED + + - id: extract + run: python .github/scripts/extract_pr_findings.py --stage install $CHANGED + + - name: Post or update sticky comment + if: always() + uses: actions/github-script@v7 + env: + BODY: ${{ steps.extract.outputs.body }} + with: + script: | + const body = process.env.BODY; + if (!body) return; + const marker = ""; + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + const existing = comments.find( + (c) => (c.body || "").includes(marker), + ); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + - name: Fail if findings + if: steps.extract.outputs.has_findings == 'true' + run: | + echo "::error::Errors detected on changed plugin entries." + exit 1 diff --git a/.github/workflows/webpage.yml b/.github/workflows/webpage.yml index 61401d9d..39d3a38b 100644 --- a/.github/workflows/webpage.yml +++ b/.github/workflows/webpage.yml @@ -1,5 +1,7 @@ --- -# Updates web page on pushes to `master` as well as daily +# Publishes plugins_metadata.json to gh-pages on pushes to `master` and daily. +# The presentation layer (browser UI) is hosted on aiida.net/plugin-registry/, +# which fetches this JSON at build time. name: webpage on: @@ -12,6 +14,9 @@ on: - master workflow_dispatch: +permissions: + contents: write + jobs: webpage: timeout-minutes: 90 @@ -23,7 +28,7 @@ jobs: COMMIT_AUTHOR_EMAIL: action@github.com steps: - name: Checkout Repo ⚡️ - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Create dev environment uses: ./.github/actions/create-dev-env @@ -33,21 +38,18 @@ jobs: gh_token: ${{ secrets.GITHUB_TOKEN }} cache: false - - uses: actions/setup-node@v3 - with: - node-version: '18.x' - - name: Install npm dependencies and build + - name: Stage JSON and redirect stubs for deploy run: | - npm install - npm run build - working-directory: ./aiida-registry-app - - - name: Add plugins file to the build folder - run: cp aiida-registry-app/src/plugins_metadata.json aiida-registry-app/dist/ + mkdir -p public + cp plugins_metadata.json public/ + # Ship pre-generated redirect stubs (root index, per-plugin pages, + # smart 404) that forward visitors from this GitHub Pages site to + # the canonical home at aiida.net/plugin-registry/. + cp -a redirects/. public/ - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./aiida-registry-app/dist + publish_dir: ./public keep_files: true diff --git a/.gitignore b/.gitignore index b503c382..ffaf08a6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,19 +8,10 @@ *.sqlite out/ installed_plugins/ -aiida-registry-app/package-lock.json # Logs logs *.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -aiida-registry-app/node_modules -aiida-registry-app/dist-ssr # data genarated plugins_metadata.json diff --git a/README.md b/README.md index 68172697..00cb7d9b 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,13 @@ This repository contains the **source** of the official registry of AiiDA plugins.

- +

+The browser UI lives at [aiida.net/plugin-registry](https://aiida.net/plugin-registry/), built from this repo's `plugins_metadata.json` (published daily to [`aiidateam.github.io/aiida-registry/plugins_metadata.json`](https://aiidateam.github.io/aiida-registry/plugins_metadata.json)). This repository owns `plugins.yaml` and the metadata-generation pipeline. + If you are starting to develop a new plugin (e.g. using the [AiiDA plugin cutter](https://github.com/aiidateam/aiida-plugin-cutter)) or if you already have one, please register it here. @@ -18,6 +20,9 @@ community of your ongoing work. By default, the list of plugins is now sorted by the latest release, so plugins that are under active development automatically bubble up to the top. The release date is determined by the date of the latest [PyPI](https://pypi.org/) release. Plugins not released to PyPI will have no release date. +## Note for maintaner of this repo: +github-pages currently serves as redirects to https://aiida.net/plugin-registry/. In order to keep the SEO scores we'll have to keep it this way for a while. + ## How to register a plugin 1. Fork this repository diff --git a/aiida-registry-app/.eslintrc.cjs b/aiida-registry-app/.eslintrc.cjs deleted file mode 100644 index ec601b2c..00000000 --- a/aiida-registry-app/.eslintrc.cjs +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:react/recommended', - 'plugin:react/jsx-runtime', - 'plugin:react-hooks/recommended', - ], - parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, - settings: { react: { version: '18.2' } }, - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': 'warn', - }, -} diff --git a/aiida-registry-app/.gitignore b/aiida-registry-app/.gitignore deleted file mode 100644 index 849ddff3..00000000 --- a/aiida-registry-app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/ diff --git a/aiida-registry-app/index.html b/aiida-registry-app/index.html deleted file mode 100644 index c7b7e625..00000000 --- a/aiida-registry-app/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - AiiDA Plugin Registry - - -
- - - diff --git a/aiida-registry-app/package.json b/aiida-registry-app/package.json deleted file mode 100644 index 8ee8cded..00000000 --- a/aiida-registry-app/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "aiida-registry", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "predeploy": "npm run build", - "deploy": "gh-pages -d aiida-registry-app/dist", - "build": "vite build", - "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" - }, - "dependencies": { - "@emotion/react": "^11.11.1", - "@emotion/styled": "^11.11.0", - "@mui/icons-material": "^5.14.0", - "@mui/material": "^5.14.0", - "fuse.js": "^6.6.2", - "markdown-to-jsx": "^7.2.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.11.2", - "html-react-parser": "^4.2.2" - }, - "devDependencies": { - "@rollup/plugin-json": "^6.0.0", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", - "@vitejs/plugin-react": "^4.0.0", - "eslint": "^8.38.0", - "eslint-plugin-react": "^7.32.2", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.3.4", - "gh-pages": "^5.0.0", - "vite": "^4.3.2" - } -} diff --git a/aiida-registry-app/public/404.html b/aiida-registry-app/public/404.html deleted file mode 100644 index 5d2bb0be..00000000 --- a/aiida-registry-app/public/404.html +++ /dev/null @@ -1,4 +0,0 @@ - - diff --git a/aiida-registry-app/public/favicon.png b/aiida-registry-app/public/favicon.png deleted file mode 100644 index c607121b..00000000 Binary files a/aiida-registry-app/public/favicon.png and /dev/null differ diff --git a/aiida-registry-app/public/status-alpha-d6af23.svg b/aiida-registry-app/public/status-alpha-d6af23.svg deleted file mode 100644 index 8d8df548..00000000 --- a/aiida-registry-app/public/status-alpha-d6af23.svg +++ /dev/null @@ -1 +0,0 @@ - statusstatusalphaalpha diff --git a/aiida-registry-app/public/status-beta-d6af23.svg b/aiida-registry-app/public/status-beta-d6af23.svg deleted file mode 100644 index e7c31fe5..00000000 --- a/aiida-registry-app/public/status-beta-d6af23.svg +++ /dev/null @@ -1 +0,0 @@ - statusstatusbetabeta diff --git a/aiida-registry-app/public/status-inactive-bbbbbb.svg b/aiida-registry-app/public/status-inactive-bbbbbb.svg deleted file mode 100644 index f893172e..00000000 --- a/aiida-registry-app/public/status-inactive-bbbbbb.svg +++ /dev/null @@ -1 +0,0 @@ -status: inactivestatusinactive diff --git a/aiida-registry-app/public/status-planning-d9644d.svg b/aiida-registry-app/public/status-planning-d9644d.svg deleted file mode 100644 index 9c282792..00000000 --- a/aiida-registry-app/public/status-planning-d9644d.svg +++ /dev/null @@ -1 +0,0 @@ - statusstatusplanningplanning diff --git a/aiida-registry-app/public/status-stable-4cc61e.svg b/aiida-registry-app/public/status-stable-4cc61e.svg deleted file mode 100644 index 980341bc..00000000 --- a/aiida-registry-app/public/status-stable-4cc61e.svg +++ /dev/null @@ -1 +0,0 @@ - statusstatusstablestable diff --git a/aiida-registry-app/rollup.config.js b/aiida-registry-app/rollup.config.js deleted file mode 100644 index f0eb8cee..00000000 --- a/aiida-registry-app/rollup.config.js +++ /dev/null @@ -1,10 +0,0 @@ -import json from '@rollup/plugin-json'; - -export default { - input: 'src/App.jsx', - output: { - dir: 'output', - format: 'cjs' - }, - plugins: [json()] -}; diff --git a/aiida-registry-app/src/App.css b/aiida-registry-app/src/App.css deleted file mode 100644 index 6ff3709f..00000000 --- a/aiida-registry-app/src/App.css +++ /dev/null @@ -1,289 +0,0 @@ -#root { - text-align: left; -} -html { - height: 100%; -} - -header { - background-color: black; - transition: top 0.5s; - display: block; - position: fixed; - top: 0; - z-index: 1; - margin: 0; - width: 100%; - padding: 5px 0px 14px 0px; -} - -body { - min-height: 100%; - font-family: 'Noto Sans', sans-serif; - padding: 0px; - margin: 0px; -} - - -p { - margin: 0; - padding: 0; -} - -a { - color: #00a; - text-decoration: none; -} - -a:visited { - color: #00a; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -h1 { - font-weight: bold; -} - -#detailsContainer { - display: flex; - margin-left: 50px; -} - -h2 { - padding-top: 16px; - margin: 0; - font-size: 140%; - color: #005; -} - -.currentstate { - color: #666; - font-size: 90%; - margin-bottom: 12px; -} -.fade-enter { - animation: fadeInAnimation ease 0.5s; - animation-iteration-count: 1; - animation-fill-mode: forwards; -} -@keyframes fadeInAnimation { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} - - -h3 { - padding-top: 20px; - padding-bottom: 5px; - margin:0; - font-size: 120%; - color: #005; -} - -h4 { - padding-top: 20px; - padding-bottom: 5px; - margin:0; -} - -ul { - margin-block-start: 0.1em; - margin-block-end: 0.1em; -} - -.footer { - display: table; - /* Distance with other content */ - width: calc(100% - 64px); - margin-top: 16px; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-right: 32px; - padding-left: 32px; - color: #555; - - font-size: 90%; -} - -.classbox { - display: inline-block; - background-color: #777; - padding: 0em .2em 0em; - font-size: 75%; - color: #fff; - border-radius: .25em -} - -/* Tooltip text */ -.classbox .tooltiptext { - visibility: hidden; - background-color: black; - color: #fff; - text-align: center; - padding: 3px; - border-radius: 6px; - - /* Position the tooltip text - see examples below! */ - position: absolute; - z-index: 1; -} - -/* Show the tooltip text when you mouse over the tooltip container */ -.classbox:hover .tooltiptext { - visibility: visible; -} - -.badge { - white-space: nowrap; - margin-right: 5px; - display: inline-block; - vertical-align: middle; - font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; -} - -span.badge-left { - border-radius: .25rem; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - color: #212529; - background-color: #A2CBFF; - text-shadow: 1px 1px 1px rgba(0,0,0,0.3); - - padding: .25em .4em; - line-height: 1; - text-align: center; - white-space: nowrap; - float: left; - display: block; -} - -span.badge-right { - border-radius: .25rem; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - - color: #fff; - background-color: #343a40; - - padding: .25em .4em; - line-height: 1; - text-align: center; - white-space: nowrap; - float: left; - display: block; -} - -.badge-right.light-blue, .badge-left.light-blue { - background-color: #A2CBFF; - color: #212529; -} - -.badge-right.light-red, .badge-left.light-red { - background-color: rgb(255, 162, 162); - color: rgb(43, 14, 14); -} - -.badge-right.red, .badge-left.red { - background-color: #e41a1c; - color: #fff; -} - -.badge-right.blue, .badge-left.blue { - background-color: #377eb8; - color: #fff; -} - -.badge-right.green, .badge-left.green { - background-color: #4daf4a; - color: #fff; -} - -.badge-right.purple, .badge-left.purple { - background-color: #984ea3; - color: #fff; -} - -.badge-right.orange, .badge-left.orange { - background-color: #ff7f00; - color: #fff; -} - -.badge-right.brown, .badge-left.brown { - background-color: #a65628; - color: #fff; -} - -.badge-right.dark-gray, .badge-left.dark-gray { - color: #fff; - background-color: #343a40; -} - - -.badge a { - text-decoration: none; - padding: 0; - border: 0; - color: inherit; -} - -.badge a:visited, .badge a:active { - color: inherit; -} - -.badge a:focus, .badge a:hover { - color: rgba(255,255,255,0.5); - mix-blend-mode: difference; - text-decoration: none; -} - - -.svg-badge { - vertical-align: middle; -} - -.tooltip { - position: relative; - display: inline-block; - border-bottom: 1px dotted black; -} - -.tooltip .tooltiptext { - visibility: hidden; - /* width: 120px; */ - background-color: rgb(255, 247, 175); - color: #000; - text-align: center; - border-radius: 6px; - padding: 5px; - - /* Position the tooltip */ - position: absolute; - z-index: 1; -} - -.tooltip:hover .tooltiptext { - visibility: visible; -} - -@media only screen and (min-width : 150px) and (max-width : 780px) - { - header - { - max-width: 100vw; - } - #detailsContainer { - margin-left: 5px; - } - .footer - { - width: calc(100% - 70px) !important; - } - } diff --git a/aiida-registry-app/src/App.jsx b/aiida-registry-app/src/App.jsx deleted file mode 100644 index afd442ac..00000000 --- a/aiida-registry-app/src/App.jsx +++ /dev/null @@ -1,114 +0,0 @@ -import { Route, Routes } from 'react-router-dom'; -import { useParams } from 'react-router-dom'; -import whiteLogo from './assets/logo-white-text.svg' -import MARVEL from './assets/MARVEL.png' -import MaX from './assets/MaX.png' -import './App.css' -import { useEffect, createContext, useState, useContext } from 'react'; -import { MainIndex } from './Components/MainIndex' -import { SearchContextProvider } from './Contexts/SearchContext'; -import { SortContextProvider } from './Contexts/SortContext'; -import Details from './Components/Details' -import Sidebar from './Components/Sidebar'; - -function App() { - - return ( - <> -
-
- - - - } /> - } /> - - - -
-