Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate plugins #198

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5fef726
Add validation script and workflow for social media plugins
arash77 Feb 20, 2025
d0094e4
Enhance validation script to provide detailed error messages for miss…
arash77 Feb 20, 2025
2c9ced5
Enhance validation script to check for missing repository secrets in …
arash77 Feb 25, 2025
1809a84
Add permissions for reading contents and actions in social plugin val…
arash77 Feb 25, 2025
b7eceb7
Remove secret validation from plugin script and clean up workflow per…
arash77 Feb 25, 2025
9eb45f6
Refactor secret validation to read plugin and workflow files directly…
arash77 Feb 25, 2025
aa140a2
Update validation script to include direct links to plugins and workf…
arash77 Feb 25, 2025
8031e71
Update validation script to use direct links for plugins and workflow…
arash77 Feb 25, 2025
87b019f
Refactor secret validation to use branch-specific references for plug…
arash77 Feb 25, 2025
ee5f06b
Refactor PR creation and README update to use branch-specific references
arash77 Feb 25, 2025
5ec723d
Improve validation script output formatting and enhance guidance for …
arash77 Feb 25, 2025
955f9a6
Refactor secret extraction to simplify enabled plugin handling and im…
arash77 Feb 25, 2025
612f0b3
Refactor PR creation to use a variable for branch name in README update
arash77 Feb 25, 2025
aedb8fd
Refactor validate_secrets and update_readme functions to remove param…
arash77 Feb 25, 2025
59d11c3
Refactor validate_plugins script to improve error handling and stream…
arash77 Feb 26, 2025
db4e47c
Refactor update_readme function to enhance YAML processing and introd…
arash77 Feb 26, 2025
c6b1ec6
Fix formatting of missing secrets message in validate_plugins script
arash77 Feb 26, 2025
6beb802
Add comment to PR creation for README update notification
arash77 Feb 26, 2025
89fb1e5
Fix regex pattern in update_readme function to match YAML block and i…
arash77 Feb 26, 2025
b9d89d5
Improve formatting of missing secrets message and enhance PR notifica…
arash77 Feb 26, 2025
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
227 changes: 227 additions & 0 deletions .github/scripts/validate_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import json
import logging
import os
import re
import sys
from datetime import datetime

import yaml
from github import Github, GithubException

logging.basicConfig(level=logging.INFO)

DEFAULT_HASHTAGS = [
"UseGalaxy",
"GalaxyProject",
"UniFreiburg",
"EOSC",
"EuroScienceGateway",
]

event_path = os.getenv("GITHUB_EVENT_PATH", "")
with open(event_path, "r") as f:
event_data = json.load(f)
pr_number = event_data.get("number")
merged = event_data.get("pull_request", {}).get("merged", False)
if event_data.get("action") == "closed" and not merged:
logging.info("No action to take")
sys.exit(0)

g = Github(os.getenv("GITHUB_TOKEN"))
repo = g.get_repo(os.getenv("GITHUB_REPOSITORY", ""))
pr = repo.get_pull(int(pr_number))
files_to_process = pr.get_files()

plugins_file = "plugins.yml"
workflow_file = os.path.join(".github", "workflows", "galaxy_social.yml")
readme_file = "README.md"


def extract_secrets_from_plugins(plugins_data):
found_secrets = set()
for plugin in plugins_data.get("plugins", []):
if not isinstance(plugin, dict):
logging.error(f"Invalid plugin data: {plugin}")
continue
config = plugin.get("config", {})
for key, value in config.items():
if isinstance(value, str) and value.startswith("$"):
found_secrets.add(value.strip("$"))
return found_secrets


def extract_secrets_from_workflow(workflow_data):
workflow_secrets = set()
for job in workflow_data.get("jobs", {}).values():
for step in job.get("steps", []):
env_vars = step.get("env", {})
for key, value in env_vars.items():
if isinstance(value, str) and "secrets." in value:
match = re.search(r"secrets\.([A-Za-z0-9_]+)", value)
if match:
workflow_secrets.add(match.group(1))
return workflow_secrets


def validate_secrets():
logging.info(f"Validating secrets in {plugins_file} and {workflow_file} ...")
head_branch = pr.head
head_repo = head_branch.repo
branch_name = head_branch.ref
plugins_contents = head_repo.get_contents(plugins_file, ref=branch_name)
plugins_data = yaml.safe_load(plugins_contents.decoded_content.decode())
workflow_contents = head_repo.get_contents(workflow_file, ref=branch_name)
workflow_data = yaml.safe_load(workflow_contents.decoded_content.decode())
if plugins_data is None or workflow_data is None:
logging.error("Failed to load plugins or workflow data.")
return

plugin_secrets = extract_secrets_from_plugins(plugins_data)
workflow_secrets = extract_secrets_from_workflow(workflow_data) - {"GITHUB_TOKEN"}

plugins_url = f"[plugins.yml]({plugins_contents.html_url})"
workflow_url = f"[galaxy_social.yml]({workflow_contents.html_url})"

errors = []
missing_in_workflow = plugin_secrets - workflow_secrets
if missing_in_workflow:
guide_lines = "\n".join(
[
f" {secret}: ${{{{ secrets.{secret} }}}}"
for secret in missing_in_workflow
]
)
errors.append(
"The following secrets are defined in **enabled plugins** in "
f"{plugins_url} but are missing from the workflow environment in "
f"{workflow_url}: `{', '.join(missing_in_workflow)}`.\n"
"Please either add them to the workflow environment or remove them from `plugins.yml`.\n"
"Make sure to add the secrets to the repository secrets as well.\n"
"For example, update your workflow to include:\n"
"```yaml\n"
f"{guide_lines}\n"
"```"
)

missing_in_plugins = workflow_secrets - plugin_secrets
if missing_in_plugins:
errors.append(
"The following secrets are defined in **workflow env** in "
f"{workflow_url} but are not used by any enabled plugin in "
f"{plugins_url}: `{', '.join(missing_in_plugins)}`.\n"
"Please either remove them from the workflow environment or ensure they are used in `plugins.yml`."
)

return errors


def update_readme_link(readme_content):
match = re.search(r"```yaml\s*(.*?)\s*```", readme_content, flags=re.DOTALL)
yaml_sample = match.group(1)
encoded_yaml = yaml_sample.replace("\n", "%0A").replace(" ", "%20")
new_link = f"../../new/main/?filename=posts/{datetime.now().year}/<your-path>.md&value={encoded_yaml}"

updated_readme = re.sub(
r'(<div align="center">.*?<kbd><a href=")([^"]+)(".*?</kbd>\s*</div>)',
rf"\1{new_link}\3",
readme_content,
flags=re.DOTALL,
)

return updated_readme


def create_pr(readme_content, readme_sha):
branch_name = f"update-readme-{pr.number}"
repo.create_git_ref(
ref=f"refs/heads/{branch_name}",
sha=repo.get_branch(pr.base.ref).commit.sha,
)
repo.update_file(
path=readme_file,
message="Update README.md",
content=readme_content,
sha=readme_sha,
branch=branch_name,
)
new_pr = repo.create_pull(
title="Update README file",
body="🔧 The `README.md` file is updated with the current media names.",
base=pr.base.ref,
head=branch_name,
)
logging.info(
f"Updated README.md with new media names\nCreated PR: {new_pr.html_url}"
)
pr.create_issue_comment(
"✅ The `README.md` file has been updated to reflect the new media names.\n"
f"A new PR has been created: {new_pr.html_url}."
)


def update_readme(head_plugins={}):
try:
readme_contents = repo.get_contents(readme_file, ref=pr.base.ref)
original_readme_content = readme_contents.decoded_content.decode("utf-8")
readme_content = original_readme_content
except Exception as e:
logging.error(f"Error fetching {readme_file}: {e}")
return

if head_plugins:
match = re.search(r"```yaml\s*---\n(.*?)\n---", readme_content, flags=re.DOTALL)
if match:
yaml_content = match.group(1)
yaml_data = yaml.safe_load(yaml_content)
for key in ("mentions", "hashtags"):
yaml_data[key] = {
plugin: value
for plugin, value in yaml_data.get(key, {}).items()
if plugin in head_plugins
}
for plugin in head_plugins:
yaml_data[key].setdefault(
plugin, [] if key == "mentions" else DEFAULT_HASHTAGS.copy()
)
yaml_data["media"] = list(head_plugins)
updated_yaml_content = yaml.dump(
yaml_data, sort_keys=False, default_flow_style=False
)
readme_content = readme_content.replace(yaml_content, updated_yaml_content)

readme_content = update_readme_link(readme_content)

if readme_content == original_readme_content:
logging.info("No change in README content. Skipping PR creation.")
return

create_pr(readme_content, readme_contents.sha)


if __name__ == "__main__":
if any(f.filename in {plugins_file, workflow_file} for f in files_to_process):
if not merged:
errors = validate_secrets()
if errors:
error_message = "⚠️ **Validation Errors Found:**\n\n" + "\n".join(
f"- {e}" for e in errors
)
logging.error(error_message)
pr.create_issue_comment(error_message)
logging.info(f"Posted comment on PR #{pr_number}")
sys.exit(1)
else:
logging.info("All validations passed successfully.")
else:
plugins_contents = pr.head.repo.get_contents(plugins_file, ref=pr.head.sha)
if isinstance(plugins_contents, list):
logging.error(f"Failed to load {plugins_file} from PR head branch.")
sys.exit(1)
plugins_data = yaml.safe_load(plugins_contents.decoded_content.decode())
head_plugins = set()
for plugin in plugins_data.get("plugins", []):
if plugin.get("enabled", False) and plugin.get("name") != "markdown":
head_plugins.add(plugin.get("name"))
update_readme(head_plugins)
elif readme_file in {f.filename for f in files_to_process} and merged:
update_readme()
32 changes: 32 additions & 0 deletions .github/workflows/validate_social_plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Validate Social Media Plugin PR

on:
pull_request_target:
branches: [main]
types: [opened, synchronize, reopened, closed]
paths:
- "plugins.yml"
- ".github/workflows/galaxy_social.yml"
- "README.md"

jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install Dependencies
run: |
pip install --upgrade pip
pip install pyyaml PyGithub

- name: Run Validation Script
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python .github/scripts/validate_plugins.py