Skip to content

Build & Deploy Web Apps to GitHub Pages #34

Build & Deploy Web Apps to GitHub Pages

Build & Deploy Web Apps to GitHub Pages #34

Workflow file for this run

name: Build & Deploy Web Apps to GitHub Pages
on:
push:
branches: [ "**" ]
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "22"
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
# Generic KMP build for all JS browser targets
- name: Build all JS browser distributions
run: |
./gradlew jsBrowserDistribution --continue
- name: Prepare static site folder (scan apps & generate index)
run: |
set -e
SITE_DIR="site"
mkdir -p "$SITE_DIR"
python << 'PYTHON_EOF'
import json, os, shutil, html
ROOT = os.getcwd()
SITE_DIR = os.path.join(ROOT, "site")
os.makedirs(SITE_DIR, exist_ok=True)
release_tag = os.environ.get("GITHUB_REF_NAME", "")
apps = []
# Walk repo and look for webapp.json
for dirpath, dirnames, filenames in os.walk(ROOT):
if "webapp.json" not in filenames:
continue
meta_path = os.path.join(dirpath, "webapp.json")
with open(meta_path, "r", encoding="utf-8") as f:
try:
meta = json.load(f)
except Exception as e:
print(f"[WARN] Cannot parse {meta_path}: {e}")
continue
app_id = meta.get("id")
name = meta.get("name", app_id)
description = meta.get("description", "")
screenshot_rel = meta.get("screenshot")
# NEW: allow distDirs list, fallback to single distDir string for compatibility
dist_dirs_rel = []
if "distDirs" in meta and isinstance(meta["distDirs"], list):
dist_dirs_rel = meta["distDirs"]
elif "distDir" in meta and isinstance(meta["distDir"], str):
dist_dirs_rel = [meta["distDir"]]
if not app_id:
print(f"[WARN] {meta_path} missing 'id', skipping.")
continue
if not dist_dirs_rel:
print(f"[WARN] {meta_path} missing 'distDirs'/'distDir', skipping.")
continue
project_root = dirpath
# Choose the first distDir that contains index.html
chosen_dist_dir = None
for rel in dist_dirs_rel:
cand_dist = os.path.join(project_root, rel)
cand_index = os.path.join(cand_dist, "index.html")
if os.path.exists(cand_index):
chosen_dist_dir = cand_dist
break
if not chosen_dist_dir:
print(f"[INFO] No index.html found in any distDir for app {app_id}, skipping.")
continue
print(f"[OK] Using dist dir {chosen_dist_dir} for app {app_id}")
# Copy dist folder into site/<id>/
target_app_dir = os.path.join(SITE_DIR, app_id)
if os.path.exists(target_app_dir):
shutil.rmtree(target_app_dir)
shutil.copytree(chosen_dist_dir, target_app_dir)
screenshot_target = None
if screenshot_rel:
screenshot_src = os.path.join(project_root, screenshot_rel)
if os.path.exists(screenshot_src):
screenshot_name = os.path.basename(screenshot_src)
screenshot_target = f"{app_id}/{screenshot_name}"
shutil.copy2(screenshot_src, os.path.join(target_app_dir, screenshot_name))
else:
print(f"[WARN] Screenshot configured but not found: {screenshot_src}")
apps.append({
"id": app_id,
"name": name or app_id,
"description": description,
"screenshot": screenshot_target
})
# Generate root index.html
apps.sort(key=lambda a: a["id"])
index_html_path = os.path.join(SITE_DIR, "index.html")
with open(index_html_path, "w", encoding="utf-8") as f:
f.write("<!DOCTYPE html>\n<html lang='en'>\n<head>\n")
f.write(" <meta charset='UTF-8'>\n")
f.write(" <meta name='viewport' content='width=device-width, initial-scale=1.0'>\n")
f.write(" <title>KMP Web Apps</title>\n")
f.write(" <style>\n")
f.write(" body { font-family: system-ui, sans-serif; max-width: 960px; margin: 2rem auto; padding: 0 1rem; }\n")
f.write(" h1 { margin-bottom: 0.25rem; }\n")
f.write(" .tag { color: #666; font-size: 0.9rem; margin-bottom: 1.5rem; }\n")
f.write(" .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; }\n")
f.write(" .card { border: 1px solid #ddd; border-radius: 0.75rem; padding: 1rem; text-decoration: none; color: inherit; background: #fafafa; transition: box-shadow 0.15s, transform 0.15s; }\n")
f.write(" .card:hover { box-shadow: 0 6px 18px rgba(0,0,0,0.08); transform: translateY(-2px); }\n")
f.write(" .card h2 { margin-top: 0; margin-bottom: 0.25rem; font-size: 1.1rem; }\n")
f.write(" .card p { margin: 0.25rem 0 0.5rem 0; font-size: 0.9rem; color: #555; }\n")
f.write(" .badge { display: inline-block; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: #555; background: #eaeaea; padding: 0.1rem 0.45rem; border-radius: 999px; margin-bottom: 0.3rem; }\n")
f.write(" .screenshot { width: 100%; border-radius: 0.5rem; margin-top: 0.5rem; border: 1px solid #e0e0e0; object-fit: cover; max-height: 180px; }\n")
f.write(" .empty { color: #777; font-style: italic; }\n")
f.write(" </style>\n")
f.write("</head>\n<body>\n")
f.write(" <h1>KMP Web Apps</h1>\n")
if release_tag:
f.write(f" <div class='tag'>Deployed from release <strong>{html.escape(release_tag)}</strong></div>\n")
if not apps:
f.write(" <p class='empty'>No web apps detected for this release (no valid <code>webapp.json</code> with a working <code>index.html</code>).</p>\n")
else:
f.write(" <div class='grid'>\n")
for app in apps:
href = f"./{app['id']}/"
name = html.escape(app["name"])
desc = html.escape(app["description"] or "")
f.write(f" <a class='card' href='{href}'>\n")
f.write(f" <div class='badge'>/{html.escape(app['id'])}</div>\n")
f.write(f" <h2>{name}</h2>\n")
if desc:
f.write(f" <p>{desc}</p>\n")
if app["screenshot"]:
f.write(f" <img class='screenshot' src='{html.escape(app['screenshot'])}' alt='Screenshot of {name}' />\n")
f.write(" </a>\n")
f.write(" </div>\n")
f.write("</body>\n</html>\n")
print(f"[OK] Root index generated at {index_html_path}")
PYTHON_EOF
- name: Upload artifact for GitHub Pages
uses: actions/upload-pages-artifact@v3
with:
path: site
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4