From 45521f95240f091c704305f40c47565045d4b876 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Sep 2025 01:40:20 +0000 Subject: [PATCH 1/2] Initial plan From 4906960cd671f61a78f6675f1c6a47388970829d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Sep 2025 01:48:11 +0000 Subject: [PATCH 2/2] Add preview and publication URL support to DAK LM and JSON generation Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com> --- .github/workflows/ghbuild.yml | 27 +++++ dak.json | 4 +- input/fsh/models/DAK.fsh | 4 +- input/pagecontent/index.md | 17 +++ input/scripts/dak_url_utils.py | 128 +++++++++++++++++++++++ input/scripts/generate_dak_from_sushi.py | 65 +++++++++++- input/scripts/pr_comment_finish.py | 24 ++++- input/scripts/pr_comment_start.py | 24 ++++- 8 files changed, 285 insertions(+), 8 deletions(-) create mode 100644 input/scripts/dak_url_utils.py diff --git a/.github/workflows/ghbuild.yml b/.github/workflows/ghbuild.yml index 1e978e544f..226db78908 100644 --- a/.github/workflows/ghbuild.yml +++ b/.github/workflows/ghbuild.yml @@ -158,12 +158,39 @@ jobs: if [ -f "dak.json" ]; then echo "✅ Found dak.json - DAK processing will be enabled" echo "DAK_ENABLED=true" >> $GITHUB_ENV + + # Set repository context for DAK URL generation + echo "GITHUB_REPOSITORY=${{ github.repository }}" >> $GITHUB_ENV + echo "GITHUB_REF_NAME=${{ github.head_ref || github.ref_name }}" >> $GITHUB_ENV + echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + else echo "⚠️ do_dak=true but no dak.json found in repository root" echo "DAK processing will be skipped" echo "DAK_ENABLED=false" >> $GITHUB_ENV fi + - name: DAK Preprocessing - Regenerate DAK configuration with current context + if: (inputs.do_dak != 'false') && (env.DAK_ENABLED == 'true') + run: | + echo "Regenerating DAK configuration with current branch context..." + + # Check if DAK generation script exists locally, download if needed + if [ ! -f "input/scripts/generate_dak_from_sushi.py" ]; then + echo "DAK generation script not found locally, downloading from smart-base repository..." + mkdir -p input/scripts + curl -L -f -o "input/scripts/generate_dak_from_sushi.py" "https://raw.githubusercontent.com/WorldHealthOrganization/smart-base/main/input/scripts/generate_dak_from_sushi.py" 2>/dev/null || echo "Failed to download generate_dak_from_sushi.py" + curl -L -f -o "input/scripts/dak_url_utils.py" "https://raw.githubusercontent.com/WorldHealthOrganization/smart-base/main/input/scripts/dak_url_utils.py" 2>/dev/null || echo "Failed to download dak_url_utils.py" + fi + + # Regenerate DAK configuration with current environment context + if [ -f "input/scripts/generate_dak_from_sushi.py" ]; then + python3 input/scripts/generate_dak_from_sushi.py + echo "✅ DAK configuration regenerated with current context" + else + echo "⚠️ DAK generation script not available, using existing dak.json" + fi + - name: DAK Preprocessing - Generate DMN Questionnaires if: (inputs.do_dak != 'false') && (env.DAK_ENABLED == 'true') run: | diff --git a/dak.json b/dak.json index 7c51d50575..756a231247 100644 --- a/dak.json +++ b/dak.json @@ -7,7 +7,9 @@ "description": "Base SMART Guidelines implementation guide to be used as the base dependency for all SMART Guidelines IGs", "version": "0.2.0", "status": "draft", - "publicationUrl": "http://smart.who.int/base", + "publicationUrl": "https://smart.who.int/base", + "previewUrl": "https://WorldHealthOrganization.github.io/smart-base", + "canonicalUrl": "https://smart.who.int/base", "license": "CC-BY-SA-3.0-IGO", "copyrightYear": "2023+", "publisher": { diff --git a/input/fsh/models/DAK.fsh b/input/fsh/models/DAK.fsh index bc6cd5ad12..1d9691028d 100644 --- a/input/fsh/models/DAK.fsh +++ b/input/fsh/models/DAK.fsh @@ -11,7 +11,9 @@ Description: "Logical Model for representing a complete Digital Adaptation Kit ( * description[x] 1..1 string or uri "DAK Description" "Description of the DAK - either Markdown content or a URI to a Markdown file (absolute or relative to repository root)" * version 1..1 string "DAK Version" "Version of the DAK" * status 1..1 code "DAK Status" "Publication status of the DAK" -* publicationUrl 1..1 url "Publication URL" "Canonical URL for the DAK (e.g., http://smart.who.int/base)" +* publicationUrl 1..1 url "Publication URL" "Canonical URL for the published DAK (e.g., https://smart.who.int/base for WHO repositories)" +* previewUrl 1..1 url "Preview URL" "Preview URL for the current CI build (e.g., https://worldhealthorganization.github.io/smart-base)" +* canonicalUrl 1..1 url "Canonical URL" "The canonical URL to use for this DAK instance - equals publicationUrl for release branches, previewUrl for development branches" * license 1..1 code "License" "License under which the DAK is published" * copyrightYear 1..1 string "Copyright Year" "Year or year range for copyright" diff --git a/input/pagecontent/index.md b/input/pagecontent/index.md index 9d645c16bc..e9d699c8ee 100644 --- a/input/pagecontent/index.md +++ b/input/pagecontent/index.md @@ -4,6 +4,23 @@ This implementation guide contains base conformance resources for use in all WHO See the [SMART IG Starter Kit](https://smart.who.int/ig-starter-kit/) for more information on building and using WHO SMART Guidelines. +### DAK (Digital Adaptation Kit) URL Handling + +For repositories that contain a `dak.json` file in the root directory, this implementation guide provides enhanced URL handling for publication and preview scenarios: + +#### Publication URLs +- **WHO Repositories**: For repositories owned by `WorldHealthOrganization`, the publication URL follows the pattern `https://smart.who.int/{stub}` where `{stub}` is the repository name with any `smart-` prefix removed. +- **Other Repositories**: Use the canonical URL specified in `sushi-config.yaml` or fall back to GitHub Pages pattern. + +#### Preview URLs +- **All Repositories**: Preview URLs use the GitHub Pages pattern `https://{profile}.github.io/{repo}` for current CI builds. + +#### Branch-Based URL Selection +- **Release Branches** (prefixed with `release-`): Use publication URLs for canonical references and resource identifiers. +- **Development Branches**: Use preview URLs for canonical references and resource identifiers. + +The DAK configuration is automatically regenerated during CI builds to ensure URLs are appropriate for the current branch context. + ### Dependencies {% include dependency-table-short.xhtml %} diff --git a/input/scripts/dak_url_utils.py b/input/scripts/dak_url_utils.py new file mode 100644 index 0000000000..1c66e3e78d --- /dev/null +++ b/input/scripts/dak_url_utils.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +DAK URL Utilities + +Shared utilities for generating publication and preview URLs for DAK-enabled repositories. +These utilities are used by PR comment scripts and other build tools to determine +the appropriate URLs based on repository configuration and branch context. +""" + +import json +import os +from pathlib import Path +from typing import Dict, Any, Optional, Tuple + + +def load_dak_config(dak_path: Path = None) -> Optional[Dict[str, Any]]: + """Load dak.json configuration if it exists.""" + if dak_path is None: + dak_path = Path("dak.json") + + if not dak_path.exists(): + return None + + try: + with open(dak_path, 'r', encoding='utf-8') as file: + return json.load(file) + except (json.JSONDecodeError, IOError): + return None + + +def generate_dak_publication_url(repo_name: str, canonical_url: str = "") -> str: + """Generate publication URL based on repository ownership and name.""" + # Check if this is a WorldHealthOrganization repository + github_repo = os.getenv('GITHUB_REPOSITORY', '') + if github_repo.startswith('WorldHealthOrganization/'): + # Extract stub by removing 'smart-' prefix if present + stub = repo_name + if stub.startswith('smart-'): + stub = stub[6:] # Remove 'smart-' prefix + return f"https://smart.who.int/{stub}" + else: + # For non-WHO repositories, use canonical URL or default pattern + if canonical_url: + return canonical_url + # Fallback to GitHub Pages pattern + if github_repo: + profile, repo = github_repo.split('/') + return f"https://{profile}.github.io/{repo}" + return canonical_url or "" + + +def generate_dak_preview_url(repo_name: str = "") -> str: + """Generate preview URL for current CI build.""" + github_repo = os.getenv('GITHUB_REPOSITORY', '') + if github_repo: + profile, repo = github_repo.split('/') + return f"https://{profile}.github.io/{repo}" + # Fallback for local development + return f"https://worldhealthorganization.github.io/{repo_name}" + + +def is_release_branch() -> bool: + """Check if current branch is a release branch (prefixed with 'release-').""" + branch_name = os.getenv('GITHUB_REF_NAME', os.getenv('BRANCH_NAME', '')) + return branch_name.startswith('release-') + + +def get_deployment_urls(branch: str, repository: str = "") -> Tuple[str, str]: + """ + Get appropriate deployment URLs based on DAK configuration and branch context. + + Returns: + Tuple[str, str]: (deployment_url, base_url) where: + - deployment_url: The URL for the specific branch deployment + - base_url: The base URL for the repository + """ + # Load DAK configuration if available + dak_config = load_dak_config() + + if dak_config: + # Use DAK-specific URL logic + repo_name = repository.split('/')[-1] if repository else "" + + if is_release_branch(): + # For release branches, use publication URL as base + base_url = dak_config.get('publicationUrl', generate_dak_publication_url(repo_name)) + else: + # For non-release branches, use preview URL as base + base_url = dak_config.get('previewUrl', generate_dak_preview_url(repo_name)) + + # Generate branch-specific URL + if branch == 'main': + deployment_url = base_url + else: + # Extract branch suffix for URL + branch_for_url = branch.split('/')[-1] if '/' in branch else branch + deployment_url = f"{base_url.rstrip('/')}/branches/{branch_for_url}" + else: + # Fallback to GitHub Pages pattern for non-DAK repositories + github_repo = repository or os.getenv('GITHUB_REPOSITORY', '') + if github_repo: + profile, repo = github_repo.split('/') + base_url = f"https://{profile}.github.io/{repo}" + else: + base_url = "https://worldhealthorganization.github.io/smart-base" + + if branch == 'main': + deployment_url = f"{base_url}/" + else: + branch_for_url = branch.split('/')[-1] if '/' in branch else branch + deployment_url = f"{base_url}/branches/{branch_for_url}/" + + return deployment_url, base_url + + +def get_canonical_url_for_branch(branch: str, repository: str = "") -> str: + """Get the canonical URL that should be used for the given branch.""" + dak_config = load_dak_config() + + if dak_config: + if is_release_branch(): + return dak_config.get('publicationUrl', dak_config.get('canonicalUrl', '')) + else: + return dak_config.get('previewUrl', dak_config.get('canonicalUrl', '')) + else: + # Fallback for non-DAK repositories + deployment_url, _ = get_deployment_urls(branch, repository) + return deployment_url \ No newline at end of file diff --git a/input/scripts/generate_dak_from_sushi.py b/input/scripts/generate_dak_from_sushi.py index d3858e3ac4..0897d81b83 100755 --- a/input/scripts/generate_dak_from_sushi.py +++ b/input/scripts/generate_dak_from_sushi.py @@ -14,6 +14,7 @@ """ import json +import os import sys import yaml from pathlib import Path @@ -48,20 +49,77 @@ def convert_publisher(sushi_publisher: Any) -> Dict[str, str]: return {"name": ""} +def generate_publication_url(repo_name: str, canonical_url: str) -> str: + """Generate publication URL based on repository ownership and name.""" + # Check if this is a WorldHealthOrganization repository + if os.getenv('GITHUB_REPOSITORY', '').startswith('WorldHealthOrganization/'): + # Extract stub by removing 'smart-' prefix if present + stub = repo_name + if stub.startswith('smart-'): + stub = stub[6:] # Remove 'smart-' prefix + return f"https://smart.who.int/{stub}" + else: + # For non-WHO repositories, use canonical URL or default pattern + if canonical_url: + return canonical_url + # Fallback to GitHub Pages pattern + github_repo = os.getenv('GITHUB_REPOSITORY', '') + if github_repo: + return f"https://{github_repo.split('/')[0]}.github.io/{github_repo.split('/')[1]}" + return canonical_url or "" + + +def generate_preview_url(repo_name: str) -> str: + """Generate preview URL for current CI build.""" + github_repo = os.getenv('GITHUB_REPOSITORY', '') + if github_repo: + profile, repo = github_repo.split('/') + return f"https://{profile}.github.io/{repo}" + # Fallback for local development + return f"https://worldhealthorganization.github.io/{repo_name}" + + +def is_release_branch() -> bool: + """Check if current branch is a release branch (prefixed with 'release-').""" + branch_name = os.getenv('GITHUB_REF_NAME', os.getenv('BRANCH_NAME', '')) + return branch_name.startswith('release-') + + def generate_dak_json(sushi_config: Dict[str, Any]) -> Dict[str, Any]: """Generate dak.json structure from sushi-config.yaml.""" + # Extract repository information + repo_id = sushi_config.get("id", "") + repo_name = repo_id.split('.')[-1] if '.' in repo_id else repo_id + canonical_url = sushi_config.get("canonical", "") + + # Generate URLs based on branch type and repository ownership + if is_release_branch(): + # For release branches, use publication URL for canonical references + publication_url = generate_publication_url(repo_name, canonical_url) + preview_url = generate_preview_url(repo_name) + # Use publication URL as canonical URL for release branches + effective_canonical = publication_url + else: + # For non-release branches, use preview URL + publication_url = generate_publication_url(repo_name, canonical_url) + preview_url = generate_preview_url(repo_name) + # Use preview URL as canonical URL for development branches + effective_canonical = preview_url + # Core DAK identity (mapped from sushi config) dak = { "resourceType": "DAK", "resourceDefinition": "http://smart.who.int/base/StructureDefinition/DAK", - "id": sushi_config.get("id", ""), + "id": repo_id, "name": sushi_config.get("name", ""), "title": sushi_config.get("title", ""), "description": sushi_config.get("description", ""), "version": sushi_config.get("version", "0.1.0"), "status": sushi_config.get("status", "draft"), - "publicationUrl": sushi_config.get("canonical", ""), + "publicationUrl": publication_url, + "previewUrl": preview_url, + "canonicalUrl": effective_canonical, "license": sushi_config.get("license", "CC0-1.0"), "copyrightYear": sushi_config.get("copyrightYear", str(datetime.now().year)), "publisher": convert_publisher(sushi_config.get("publisher", {})) @@ -103,6 +161,9 @@ def main(): print(f"DAK ID: {dak_config['id']}") print(f"DAK Title: {dak_config['title']}") print(f"Publication URL: {dak_config['publicationUrl']}") + print(f"Preview URL: {dak_config['previewUrl']}") + print(f"Canonical URL: {dak_config['canonicalUrl']}") + print(f"Is Release Branch: {is_release_branch()}") except IOError as e: print(f"Error writing output file: {e}") sys.exit(1) diff --git a/input/scripts/pr_comment_finish.py b/input/scripts/pr_comment_finish.py index 929b6612ed..daeb6dd186 100755 --- a/input/scripts/pr_comment_finish.py +++ b/input/scripts/pr_comment_finish.py @@ -16,6 +16,13 @@ import requests from urllib.parse import quote +# Try to import DAK URL utilities, fallback to inline implementation if not available +try: + from dak_url_utils import get_deployment_urls + DAK_UTILS_AVAILABLE = True +except ImportError: + DAK_UTILS_AVAILABLE = False + def sanitize_input(value: str) -> str: """Sanitize input to prevent injection attacks.""" @@ -67,16 +74,29 @@ def validate_job_status(status: str) -> str: def generate_deployment_url(branch: str) -> str: """Generate deployment URL with proper sanitization.""" branch = sanitize_input(branch) + repository = os.getenv('GITHUB_REPOSITORY', 'WorldHealthOrganization/smart-base') + + # Use DAK-aware URL generation if available + if DAK_UTILS_AVAILABLE: + try: + deployment_url, _ = get_deployment_urls(branch, repository) + return deployment_url + except Exception: + # Fallback to default implementation if DAK utils fail + pass + # Default GitHub Pages implementation if branch == 'main': - return 'https://worldhealthorganization.github.io/smart-base/' + return f'https://{repository.split("/")[0].lower()}.github.io/{repository.split("/")[1]}/' else: # Extract branch suffix after last slash for URL branch_for_url = branch.split('/')[-1] if '/' in branch else branch branch_for_url = sanitize_input(branch_for_url) # URL encode the branch name for safety branch_encoded = quote(branch_for_url, safe='') - return f'https://worldhealthorganization.github.io/smart-base/branches/{branch_encoded}/' + profile = repository.split('/')[0].lower() + repo = repository.split('/')[1] + return f'https://{profile}.github.io/{repo}/branches/{branch_encoded}/' def update_pr_comment(pr_number: int, repository: str, run_id: int, sha: str, branch: str, job_status: str, github_token: str): diff --git a/input/scripts/pr_comment_start.py b/input/scripts/pr_comment_start.py index 906143bb31..3ceca0f47c 100755 --- a/input/scripts/pr_comment_start.py +++ b/input/scripts/pr_comment_start.py @@ -16,6 +16,13 @@ import requests from urllib.parse import quote +# Try to import DAK URL utilities, fallback to inline implementation if not available +try: + from dak_url_utils import get_deployment_urls + DAK_UTILS_AVAILABLE = True +except ImportError: + DAK_UTILS_AVAILABLE = False + def sanitize_input(value: str) -> str: """Sanitize input to prevent injection attacks.""" @@ -55,16 +62,29 @@ def validate_run_id(run_id: str) -> int: def generate_deployment_url(branch: str) -> str: """Generate deployment URL with proper sanitization.""" branch = sanitize_input(branch) + repository = os.getenv('GITHUB_REPOSITORY', 'WorldHealthOrganization/smart-base') + + # Use DAK-aware URL generation if available + if DAK_UTILS_AVAILABLE: + try: + deployment_url, _ = get_deployment_urls(branch, repository) + return deployment_url + except Exception: + # Fallback to default implementation if DAK utils fail + pass + # Default GitHub Pages implementation if branch == 'main': - return 'https://worldhealthorganization.github.io/smart-base/' + return f'https://{repository.split("/")[0].lower()}.github.io/{repository.split("/")[1]}/' else: # Extract branch suffix after last slash for URL branch_for_url = branch.split('/')[-1] if '/' in branch else branch branch_for_url = sanitize_input(branch_for_url) # URL encode the branch name for safety branch_encoded = quote(branch_for_url, safe='') - return f'https://worldhealthorganization.github.io/smart-base/branches/{branch_encoded}/' + profile = repository.split('/')[0].lower() + repo = repository.split('/')[1] + return f'https://{profile}.github.io/{repo}/branches/{branch_encoded}/' def post_pr_comment(pr_number: int, repository: str, run_id: int, sha: str, branch: str, github_token: str) -> str: