name: FHIR IG CI/CD Pipeline with Version Persistence on: push: tags: - 'v*.*.*' pull_request: branches: [ main ] env: REGISTRY: git.dghs.gov.bd IMAGE_NAME: gitadmin/bd-core-fhir-ig jobs: build-ig: runs-on: fhir-runner steps: - name: Checkout repository uses: actions/checkout@v3 with: fetch-depth: 0 - name: Extract version from IG id: version run: | VERSION=$(grep -oP '> $GITHUB_OUTPUT if [[ "$GITHUB_REF" == refs/tags/v* ]]; then BUILD_TYPE="release" TAG_VERSION="${GITHUB_REF#refs/tags/v}" if [ "$TAG_VERSION" != "$VERSION" ]; then echo "ERROR: Git tag version ($TAG_VERSION) doesn't match IG version ($VERSION)" exit 1 fi else BUILD_TYPE="dev" fi echo "build_type=$BUILD_TYPE" >> $GITHUB_OUTPUT echo "Build type: $BUILD_TYPE" - name: Prepare package-list.json and history.xml for IG Publisher run: | VERSION="${{ steps.version.outputs.version }}" BUILD_TYPE="${{ steps.version.outputs.build_type }}" DATE=$(date +%Y-%m-%d) export VERSION DATE BUILD_TYPE echo "📋 Preparing package-list.json and history.xml for IG Publisher..." if [ ! -f "package-list.json" ]; then echo "âš ī¸ package-list.json not found in repo root" echo "Creating initial package-list.json..." cat > package-list.json << 'PKGEOF' { "package-id": "bd.fhir.core", "title": "Bangladesh Core FHIR Implementation Guide", "canonical": "https://fhir.dghs.gov.bd/core", "introduction": "The Bangladesh Core FHIR IG defines national base profiles, value sets, and extensions for health data interoperability.", "list": [ { "version": "current", "desc": "Continuous Integration Build (latest in version control)", "path": "https://fhir.dghs.gov.bd/core/", "status": "ci-build", "current": true } ] } PKGEOF fi python3 << 'PYEOF' import json import os import sys version = os.environ.get('VERSION', '') date = os.environ.get('DATE', '') build_type = os.environ.get('BUILD_TYPE', '') with open('package-list.json', 'r', encoding='utf-8') as f: pkg_list = json.load(f) if 'list' not in pkg_list or not isinstance(pkg_list['list'], list): print("ERROR: package-list.json does not contain a valid 'list' array") sys.exit(1) current_entries = [e for e in pkg_list['list'] if e.get('version') == 'current'] if not current_entries: pkg_list['list'].insert(0, { "version": "current", "desc": "Continuous Integration Build (latest in version control)", "path": "https://fhir.dghs.gov.bd/core/", "status": "ci-build", "current": True }) if build_type == 'release': version_entry = None for e in pkg_list['list']: if e.get('version') == version: version_entry = e break if version_entry is None: new_entry = { "version": version, "date": date, "desc": f"Release {version}", "path": f"https://fhir.dghs.gov.bd/core/{version}/", "status": "trial-use", "sequence": "STU 1" } insert_index = 1 for i, entry in enumerate(pkg_list['list']): if entry.get('version') == 'current': insert_index = i + 1 break pkg_list['list'].insert(insert_index, new_entry) print(f"✅ Added version {version} to package-list.json") else: print(f"â„šī¸ Version {version} already exists in package-list.json") else: print("â„šī¸ Dev build - using existing package-list.json without release modification") with open('package-list.json', 'w', encoding='utf-8') as f: json.dump(pkg_list, f, indent=2, ensure_ascii=False) PYEOF echo "🔍 Validating package-list.json..." python3 -m json.tool package-list.json > /dev/null && echo "✅ Valid JSON" || (echo "❌ Invalid JSON!" && exit 1) echo "📂 Ensuring package-list.json is in required locations..." mkdir -p input cp package-list.json input/package-list.json echo "📝 Generating static history.xml from package-list.json..." mkdir -p input/pagecontent python3 << 'PYEOF' import json import os from html import escape os.makedirs('input/pagecontent', exist_ok=True) with open('package-list.json', 'r', encoding='utf-8') as f: pkg_list = json.load(f) xml = '''

This page provides the version history for the Bangladesh Core FHIR Implementation Guide.

For a machine-readable version history see package-list.json.

Published Versions

''' def version_key(v): try: return tuple(int(x) for x in v.split('.')) except: return (0,) published = [ e for e in pkg_list['list'] if e.get('version') and e.get('version') != "current" ] # Sort newest version first published.sort( key=lambda e: version_key(e.get("version", "0.0.0")), reverse=True ) published_found = False first_row = True for entry in published: published_found = True version = escape(entry.get('version', 'Unknown')) date = escape(entry.get('date', 'N/A')) desc = escape(entry.get('desc', '')) path = escape(entry.get('path', '#')) status_val = entry.get('status', 'unknown') if status_val == "trial-use": status = 'Trial Use' elif status_val == "normative": status = 'Normative' else: status = f'{escape(status_val)}' badge = 'Latest ' if first_row else '' xml += f''' ''' first_row = False if not published_found: xml += ''' ''' xml += '''
Version Date Status Description
{badge}{version} {date} {status} {desc}
No published versions available yet.

Continuous Integration Build

''' current_entry = None for entry in pkg_list['list']: if entry.get('version') == 'current': current_entry = entry break if current_entry: path = escape(current_entry.get('path', pkg_list.get('canonical', '') + '/')) xml += f'''

The latest development build is available at: {path}

''' else: xml += '''

No CI build entry found in package-list.json.

''' xml += '''
''' with open('input/pagecontent/history.xml', 'w', encoding='utf-8') as f: f.write(xml) print("✅ Generated static history.xml") print(f" File location: {os.path.abspath('input/pagecontent/history.xml')}") print(f" File size: {os.path.getsize('input/pagecontent/history.xml')} bytes") PYEOF if [ -f "input/pagecontent/history.xml" ]; then echo "✅ Verified: history.xml exists" echo " First 20 lines:" head -20 input/pagecontent/history.xml else echo "❌ ERROR: history.xml was not created!" exit 1 fi echo "✅ Pre-build preparation complete:" echo " - Root: $(pwd)/package-list.json" echo " - Input: $(pwd)/input/package-list.json" echo " - History: $(pwd)/input/pagecontent/history.xml" echo "===============================" echo "PACKAGE LIST USED FOR BUILD:" cat package-list.json echo "-------------------------------" echo "INPUT COPY:" cat input/package-list.json echo "===============================" - name: Emergency Disk Cleanup run: | echo "Disk usage before:" df -h echo "Clearing tool cache..." rm -rf /opt/hostedtoolcache/* || true rm -rf /usr/share/dotnet || true rm -rf /usr/local/lib/android || true rm -rf /opt/ghc || true rm -rf ~/.fhir/packages || true echo "Disk usage after:" df -h - name: Install Docker CLI run: | apt-get update apt-get install -y docker.io docker --version - name: Preload previous IG package for comparison run: | echo "Detecting previous version..." PREV_VERSION=$(python3 <<'PY' import json import os current_build = os.environ.get("VERSION") with open("package-list.json") as f: data = json.load(f) def parse(v): try: return tuple(int(x) for x in v.split(".")) except: return (0,) versions = [ v["version"] for v in data["list"] if v.get("version") not in ("current", current_build) ] versions.sort(key=parse, reverse=True) print(versions[0] if versions else "") PY ) if [ -z "$PREV_VERSION" ]; then echo "No previous version found. Skipping preload." exit 0 fi echo "Previous version detected: $PREV_VERSION" mkdir -p previous-packages TMPDIR=$(mktemp -d) URL="https://fhir.dghs.gov.bd/core/$PREV_VERSION/package.tgz" echo "Downloading $URL" curl -fL "$URL" -o "$TMPDIR/package.tgz" mkdir -p "previous-packages/bd.fhir.core#$PREV_VERSION" tar -xzf "$TMPDIR/package.tgz" -C "previous-packages/bd.fhir.core#$PREV_VERSION" echo "Previous package cached:" ls previous-packages - name: Build FHIR IG run: | echo "Building FHIR IG version ${{ steps.version.outputs.version }}..." CONTAINER_ID=$(docker create \ -v $(pwd)/previous-packages:/previous-packages \ hl7fhir/ig-publisher-base:latest \ /bin/bash -c " mkdir -p /tmp/build cp -r /home/publisher/ig /tmp/build/ig cd /tmp/build/ig rm -f package-list.json _updatePublisher.sh -y _genonce.sh ") echo "Container ID: $CONTAINER_ID" docker cp "$(pwd)/." "$CONTAINER_ID:/home/publisher/ig/" echo "Mounted FHIR packages:" ls -R previous-packages || echo "No previous packages directory" docker start -a "$CONTAINER_ID" EXIT_CODE=$? echo "Copying outputs from container..." docker cp "$CONTAINER_ID:/tmp/build/ig/output" ./output || echo "Warning: No output directory" docker cp "$CONTAINER_ID:/tmp/build/ig/fsh-generated" ./fsh-generated || echo "No FSH generated" docker cp "$CONTAINER_ID:/tmp/build/ig/input-cache" ./input-cache || echo "No input-cache" docker cp "$CONTAINER_ID:/tmp/build/ig/temp" ./temp || echo "No temp directory" if [ $EXIT_CODE -ne 0 ]; then echo "Build failed, showing logs:" docker logs "$CONTAINER_ID" docker rm "$CONTAINER_ID" exit 1 fi docker rm "$CONTAINER_ID" if [ ! -f "output/index.html" ]; then echo "ERROR: Build failed - no index.html" exit 1 fi echo "" echo "🔍 Checking for history.html..." if [ -f "output/history.html" ]; then echo "✅ history.html generated successfully!" echo "📄 history.html size: $(ls -lh output/history.html | awk '{print $5}')" else echo "âš ī¸ WARNING: history.html was NOT generated" echo "This might indicate an issue with the template or history.xml/package-list.json" fi echo "================================" echo "IG Publisher comparison log:" cat output/qa.compare.txt || echo "qa.compare.txt not found" echo "================================" echo "✅ Build successful!" - name: Update package-feed.xml for releases if: steps.version.outputs.build_type == 'release' run: | VERSION="${{ steps.version.outputs.version }}" DATETIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) cat > update-feed.py << 'EOF' import sys import xml.etree.ElementTree as ET version = sys.argv[1] datetime_iso = sys.argv[2] ET.register_namespace('', 'http://www.w3.org/2005/Atom') tree = ET.parse('package-feed.xml') root = tree.getroot() ns = {'atom': 'http://www.w3.org/2005/Atom'} updated_elem = root.find('atom:updated', ns) if updated_elem is not None: updated_elem.text = datetime_iso entry_exists = False for entry in root.findall('atom:entry', ns): title = entry.find('atom:title', ns) if title is not None and version in (title.text or ''): entry_exists = True entry_updated = entry.find('atom:updated', ns) if entry_updated is not None: entry_updated.text = datetime_iso break if not entry_exists: new_entry = ET.Element('{http://www.w3.org/2005/Atom}entry') title = ET.SubElement(new_entry, '{http://www.w3.org/2005/Atom}title') title.text = f"bd.fhir.core version {version}" link = ET.SubElement(new_entry, '{http://www.w3.org/2005/Atom}link') link.set('rel', 'alternate') link.set('href', f"https://fhir.dghs.gov.bd/core/{version}/") entry_id = ET.SubElement(new_entry, '{http://www.w3.org/2005/Atom}id') entry_id.text = f"https://fhir.dghs.gov.bd/core/{version}/" entry_updated = ET.SubElement(new_entry, '{http://www.w3.org/2005/Atom}updated') entry_updated.text = datetime_iso summary = ET.SubElement(new_entry, '{http://www.w3.org/2005/Atom}summary') summary.text = f"Release {version} of Bangladesh Core FHIR Implementation Guide" insert_pos = 0 for i, child in enumerate(root): if child.tag.endswith('entry'): insert_pos = i break insert_pos = i + 1 root.insert(insert_pos, new_entry) tree.write('output/package-feed.xml', encoding='utf-8', xml_declaration=True) print("✅ Updated package-feed.xml") EOF python3 update-feed.py "$VERSION" "$DATETIME" cp package-list.json output/package-list.json echo "📋 Updated registry files" - name: Prepare deployment artifact run: | VERSION="${{ steps.version.outputs.version }}" BUILD_TYPE="${{ steps.version.outputs.build_type }}" tar -czf ig-output.tar.gz -C output . echo "version=$VERSION" > deployment.env echo "build_type=$BUILD_TYPE" >> deployment.env echo "build_date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> deployment.env echo "đŸ“Ļ Output contents:" ls -lh output/ | grep -E "(history\.html|package-list\.json|package-feed\.xml|index\.html)" || echo "Some files may be missing" ls -lh ig-output.tar.gz - name: Upload artifact uses: actions/upload-artifact@v3 with: name: ig-output path: | ig-output.tar.gz deployment.env package-list.json package-feed.xml retention-days: 30 deploy: needs: build-ig runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') steps: - name: Download artifact uses: actions/download-artifact@v3 with: name: ig-output - name: Load deployment env id: deploy_info run: | source deployment.env echo "version=$version" >> $GITHUB_OUTPUT echo "build_type=$build_type" >> $GITHUB_OUTPUT echo "build_date=$build_date" >> $GITHUB_OUTPUT echo "Deploying version: $version" echo "Build type: $build_type" - name: Deploy to server uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} password: ${{ secrets.DEPLOY_PASSWORD }} port: ${{ secrets.DEPLOY_PORT || 22 }} source: "ig-output.tar.gz,deployment.env,package-list.json,package-feed.xml" target: "/tmp/fhir-ig-deploy/" - name: Execute deployment on server uses: appleboy/ssh-action@v1.0.3 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} password: ${{ secrets.DEPLOY_PASSWORD }} port: ${{ secrets.DEPLOY_PORT || 22 }} script: | set -e source /tmp/fhir-ig-deploy/deployment.env echo "==========================================" echo "Deploying FHIR IG" echo "Version: $version" echo "Build Type: $build_type" echo "Build Date: $build_date" echo "==========================================" VERSIONS_DIR="/opt/fhir-ig/versions" mkdir -p "$VERSIONS_DIR" if [ "$build_type" = "release" ]; then TARGET_DIR="$VERSIONS_DIR/$version" echo "đŸ“Ļ Deploying release version to: $TARGET_DIR" else TARGET_DIR="$VERSIONS_DIR/dev" echo "🔧 Deploying dev build to: $TARGET_DIR" mkdir -p "$TARGET_DIR" echo "Cleaning old dev files..." rm -rf "$TARGET_DIR"/* fi mkdir -p "$TARGET_DIR" echo "Extracting IG output..." tar -xzf /tmp/fhir-ig-deploy/ig-output.tar.gz -C "$TARGET_DIR" if [ -f "$TARGET_DIR/history.html" ]; then echo "✅ history.html deployed successfully" else echo "âš ī¸ WARNING: history.html not found in deployment" fi cp "$TARGET_DIR/package-list.json" "$VERSIONS_DIR/package-list.json" cp "$TARGET_DIR/package-feed.xml" "$VERSIONS_DIR/package-feed.xml" cp "$TARGET_DIR/package-list.json" "/opt/fhir-ig/package-list.json" cp "$TARGET_DIR/package-feed.xml" "/opt/fhir-ig/package-feed.xml" if [ "$build_type" = "release" ]; then echo "Updating 'current' symlink to point to $version" rm -f "$VERSIONS_DIR/current" ln -sf "$version" "$VERSIONS_DIR/current" fi cd /opt/fhir-ig if [ ! -f "docker-compose.prod.yml" ]; then echo "ERROR: docker-compose.prod.yml not found!" echo "Please deploy the updated docker-compose.prod.yml and nginx.conf first" exit 1 fi docker compose -f docker-compose.prod.yml up -d --force-recreate fhir-ig rm -rf /tmp/fhir-ig-deploy echo "==========================================" echo "✅ Deployment completed successfully!" echo "Version $version is now available at:" if [ "$build_type" = "release" ]; then echo " - https://fhir.dghs.gov.bd/core/$version/" echo " - https://fhir.dghs.gov.bd/core/$version/history.html" echo " - https://fhir.dghs.gov.bd/core/ (current)" else echo " - https://fhir.dghs.gov.bd/core/dev/" fi echo "==========================================" echo "Available versions:" ls -lh "$VERSIONS_DIR" | grep -v total