|
| 1 | +name: Deploy to GitHub Pages |
| 2 | + |
| 3 | +on: |
| 4 | + push: |
| 5 | + branches: ["main"] |
| 6 | + workflow_dispatch: |
| 7 | + |
| 8 | +permissions: |
| 9 | + contents: read |
| 10 | + pages: write |
| 11 | + id-token: write |
| 12 | + |
| 13 | +concurrency: |
| 14 | + group: "pages" |
| 15 | + cancel-in-progress: false |
| 16 | + |
| 17 | +jobs: |
| 18 | + build: |
| 19 | + runs-on: ubuntu-latest |
| 20 | + steps: |
| 21 | + - name: Checkout |
| 22 | + uses: actions/checkout@v4 |
| 23 | + |
| 24 | + - name: Generate Jekyll site |
| 25 | + shell: bash |
| 26 | + env: |
| 27 | + REPO_NAME: ${{ github.event.repository.name }} |
| 28 | + REPO_FULL: ${{ github.repository }} |
| 29 | + REPO_DESC: ${{ github.event.repository.description }} |
| 30 | + run: | |
| 31 | + set -e |
| 32 | +
|
| 33 | + OWNER="${REPO_FULL%%/*}" |
| 34 | + REPO="${REPO_NAME}" |
| 35 | + DESC="${REPO_DESC:-$REPO}" |
| 36 | + BASE_URL="/${REPO}" |
| 37 | + STAGE="_site_source" |
| 38 | +
|
| 39 | + echo "::group::Staging allowed files" |
| 40 | +
|
| 41 | + # ── Stage ONLY allowed files into a clean directory ─────────── |
| 42 | + # Whitelist: *.md files + LICENSE + LICENSE.txt |
| 43 | + # Everything else is excluded from the site. |
| 44 | + mkdir -p "${STAGE}" |
| 45 | +
|
| 46 | + # Copy all .md files preserving directory structure |
| 47 | + find . -name '*.md' \ |
| 48 | + -not -path './.git/*' \ |
| 49 | + -not -path "./${STAGE}/*" \ |
| 50 | + | while read -r f; do |
| 51 | + dest="${STAGE}/${f#./}" |
| 52 | + mkdir -p "$(dirname "$dest")" |
| 53 | + cp "$f" "$dest" |
| 54 | + done |
| 55 | +
|
| 56 | + # Copy LICENSE files (plain text) |
| 57 | + for lf in LICENSE LICENSE.txt; do |
| 58 | + [ -f "$lf" ] && cp "$lf" "${STAGE}/" |
| 59 | + done |
| 60 | +
|
| 61 | + # Remove any .gitkeep files that got copied |
| 62 | + find "${STAGE}" -name '.gitkeep' -delete |
| 63 | +
|
| 64 | + echo "Staged files:" |
| 65 | + find "${STAGE}" -type f | sort |
| 66 | + echo "::endgroup::" |
| 67 | +
|
| 68 | + # ── Everything below operates inside the staging dir ────────── |
| 69 | + cd "${STAGE}" |
| 70 | +
|
| 71 | + echo "::group::Generating Jekyll config and layout" |
| 72 | +
|
| 73 | + # ── _config.yml ─────────────────────────────────────────────── |
| 74 | + cat > _config.yml << CONFIGEOF |
| 75 | + title: "${REPO}" |
| 76 | + description: "${DESC}" |
| 77 | + permalink: pretty |
| 78 | + baseurl: "${BASE_URL}" |
| 79 | +
|
| 80 | + defaults: |
| 81 | + - scope: |
| 82 | + path: "docs" |
| 83 | + values: |
| 84 | + layout: default |
| 85 | + nav_section: docs |
| 86 | + CONFIGEOF |
| 87 | +
|
| 88 | + # ── _layouts/default.html ───────────────────────────────────── |
| 89 | + mkdir -p _layouts |
| 90 | + cat > _layouts/default.html << 'LAYOUTEOF' |
| 91 | + <!DOCTYPE html> |
| 92 | + <html lang="en"> |
| 93 | + <head> |
| 94 | + <meta charset="UTF-8"> |
| 95 | + <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 96 | + <title>{{ page.title | default: site.title }}</title> |
| 97 | + <link rel="preconnect" href="https://fonts.googleapis.com"> |
| 98 | + <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet"> |
| 99 | + <style> |
| 100 | + *{margin:0;padding:0;box-sizing:border-box} |
| 101 | + body{font-family:'IBM Plex Sans',-apple-system,sans-serif;color:#24292f;background:#f6f8fa;line-height:1.7} |
| 102 | + a{color:#0969da;text-decoration:none} |
| 103 | + a:hover{text-decoration:underline} |
| 104 | + .site-header{background:#fff;border-bottom:1px solid #d0d7de;padding:16px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px} |
| 105 | + .site-title{font-family:'IBM Plex Mono',monospace;font-size:16px;font-weight:500;color:#24292f;text-decoration:none} |
| 106 | + .site-nav{display:flex;gap:20px;flex-wrap:wrap} |
| 107 | + .site-nav a{font-size:14px;color:#57606a;text-decoration:none;padding:4px 0} |
| 108 | + .site-nav a:hover{color:#24292f} |
| 109 | + .site-nav a.active{color:#24292f;font-weight:500;border-bottom:2px solid #fd8c73} |
| 110 | + .container{max-width:820px;margin:32px auto;padding:0 24px} |
| 111 | + .content{background:#fff;border:1px solid #d0d7de;border-radius:6px;padding:32px} |
| 112 | + .content h1{font-size:24px;font-weight:500;margin:0 0 16px;border-bottom:1px solid #d0d7de;padding-bottom:8px} |
| 113 | + .content h2{font-size:20px;font-weight:500;margin:24px 0 12px;border-bottom:1px solid #d0d7de;padding-bottom:6px} |
| 114 | + .content h3{font-size:16px;font-weight:500;margin:20px 0 8px} |
| 115 | + .content h4{font-size:14px;font-weight:500;margin:16px 0 6px} |
| 116 | + .content p{font-size:14px;color:#57606a;margin:0 0 16px} |
| 117 | + .content ul,.content ol{font-size:14px;color:#57606a;margin:0 0 16px;padding-left:24px} |
| 118 | + .content li{margin-bottom:4px} |
| 119 | + .content code{font-family:'IBM Plex Mono',monospace;font-size:13px;background:#f6f8fa;padding:2px 6px;border-radius:4px} |
| 120 | + .content pre{background:#f6f8fa;border:1px solid #d0d7de;border-radius:6px;padding:16px;overflow-x:auto;margin:8px 0 16px;line-height:1.5} |
| 121 | + .content pre code{background:transparent;padding:0} |
| 122 | + .content table{width:100%;border-collapse:collapse;margin:8px 0 16px;font-size:14px} |
| 123 | + .content th,.content td{border:1px solid #d0d7de;padding:8px 12px;text-align:left} |
| 124 | + .content th{background:#f6f8fa;font-weight:500} |
| 125 | + .content img{max-width:100%} |
| 126 | + #releases-container .release{border-bottom:1px solid #d0d7de;padding:20px 0} |
| 127 | + #releases-container .release:last-child{border-bottom:none} |
| 128 | + .release-tag{font-family:'IBM Plex Mono',monospace;font-size:16px;font-weight:500;color:#0969da} |
| 129 | + .release-date{font-size:13px;color:#8b949e;margin-left:8px} |
| 130 | + .release-latest{font-size:11px;padding:2px 8px;border-radius:16px;background:#dafbe1;color:#1a7f37;font-weight:500;margin-left:8px} |
| 131 | + .release-body{font-size:14px;color:#57606a;margin-top:8px;line-height:1.7} |
| 132 | + .release-body h2,.release-body h3{font-size:14px;font-weight:500;color:#24292f;margin:12px 0 4px;border:none;padding:0} |
| 133 | + .release-body ul{padding-left:20px} |
| 134 | + .release-body li{margin-bottom:2px;font-size:13px} |
| 135 | + .release-assets{margin-top:12px} |
| 136 | + .release-assets summary{font-size:13px;font-weight:500;cursor:pointer;color:#24292f} |
| 137 | + .release-asset{font-family:'IBM Plex Mono',monospace;font-size:12px;color:#57606a;padding:3px 0} |
| 138 | + .loading{text-align:center;padding:32px;color:#8b949e;font-size:14px} |
| 139 | + @media(max-width:640px){.site-header{padding:12px 16px}.container{padding:0 12px;margin:16px auto}.content{padding:20px}} |
| 140 | + </style> |
| 141 | + </head> |
| 142 | + <body> |
| 143 | + <header class="site-header"> |
| 144 | + <a class="site-title" href="{{ '/' | relative_url }}">{{ site.title }}</a> |
| 145 | + <nav class="site-nav"> |
| 146 | + <a href="{{ '/' | relative_url }}" {% if page.url == '/' %}class="active"{% endif %}>Home</a> |
| 147 | + {% assign has_docs = false %}{% for p in site.pages %}{% if p.path contains 'docs/' and p.name != 'index.md' %}{% assign has_docs = true %}{% break %}{% endif %}{% endfor %} |
| 148 | + {% if has_docs %}<a href="{{ '/docs/' | relative_url }}" {% if page.url contains '/docs' %}class="active"{% endif %}>Docs</a>{% endif %} |
| 149 | + <a href="{{ '/releases/' | relative_url }}" {% if page.url contains '/releases' %}class="active"{% endif %}>Releases</a> |
| 150 | + {% for p in site.pages %}{% if p.path == 'LICENSE.md' %}<a href="{{ '/license/' | relative_url }}" {% if page.url contains '/license' %}class="active"{% endif %}>License</a>{% break %}{% endif %}{% endfor %} |
| 151 | + {% for p in site.pages %}{% if p.path == 'CONTRIBUTING.md' %}<a href="{{ '/contributing/' | relative_url }}" {% if page.url contains '/contributing' %}class="active"{% endif %}>Contributing</a>{% break %}{% endif %}{% endfor %} |
| 152 | + {% for p in site.pages %}{% if p.path == 'CODE_OF_CONDUCT.md' %}<a href="{{ '/code-of-conduct/' | relative_url }}" {% if page.url contains '/code-of-conduct' %}class="active"{% endif %}>Code of conduct</a>{% break %}{% endif %}{% endfor %} |
| 153 | + </nav> |
| 154 | + </header> |
| 155 | + <main class="container"> |
| 156 | + <div class="content">{{ content }}</div> |
| 157 | + </main> |
| 158 | + </body> |
| 159 | + </html> |
| 160 | + LAYOUTEOF |
| 161 | + echo "::endgroup::" |
| 162 | +
|
| 163 | + echo "::group::Processing content files" |
| 164 | +
|
| 165 | + # ── README.md → homepage ────────────────────────────────────── |
| 166 | + if [ -f "README.md" ] && ! head -1 README.md | grep -q '^\-\-\-'; then |
| 167 | + TEMP=$(mktemp) |
| 168 | + printf -- '---\nlayout: default\ntitle: Home\npermalink: /\n---\n\n' > "$TEMP" |
| 169 | + cat README.md >> "$TEMP" |
| 170 | + mv "$TEMP" README.md |
| 171 | + echo "Processed README.md → homepage" |
| 172 | + fi |
| 173 | +
|
| 174 | + # ── Docs: add front matter + create index ───────────────────── |
| 175 | + if [ -d "docs" ]; then |
| 176 | + for f in docs/*.md; do |
| 177 | + [ -f "$f" ] || continue |
| 178 | + [ "$(basename "$f")" = "index.md" ] && continue |
| 179 | +
|
| 180 | + if ! head -1 "$f" | grep -q '^\-\-\-'; then |
| 181 | + BASENAME=$(basename "$f" .md) |
| 182 | + SYNOPSIS=$(grep -m1 -A1 '## Synopsis' "$f" 2>/dev/null | tail -1 | sed 's/^[[:space:]]*//') |
| 183 | + [ -z "$SYNOPSIS" ] && SYNOPSIS="$BASENAME" |
| 184 | + TEMP=$(mktemp) |
| 185 | + printf -- '---\nlayout: default\ntitle: %s\ndescription: "%s"\n---\n\n' "$BASENAME" "$SYNOPSIS" > "$TEMP" |
| 186 | + cat "$f" >> "$TEMP" |
| 187 | + mv "$TEMP" "$f" |
| 188 | + echo "Processed $f" |
| 189 | + fi |
| 190 | + done |
| 191 | +
|
| 192 | + if [ ! -f "docs/index.md" ]; then |
| 193 | + cat > docs/index.md << 'DOCSEOF' |
| 194 | + --- |
| 195 | + layout: default |
| 196 | + title: Docs |
| 197 | + permalink: /docs/ |
| 198 | + --- |
| 199 | + # Function reference |
| 200 | + {% assign doc_pages = site.pages | where_exp: "p", "p.path contains 'docs/'" | where_exp: "p", "p.name != 'index.md'" | sort: "title" %} |
| 201 | + {% if doc_pages.size > 0 %} |
| 202 | + <ul> |
| 203 | + {% for doc in doc_pages %} |
| 204 | + <li><a href="{{ doc.url | relative_url }}"><code>{{ doc.title }}</code></a> — {{ doc.description | default: doc.title }}</li> |
| 205 | + {% endfor %} |
| 206 | + </ul> |
| 207 | + {% else %} |
| 208 | + <p><em>No function documentation found yet.</em></p> |
| 209 | + {% endif %} |
| 210 | + DOCSEOF |
| 211 | + echo "Created docs/index.md" |
| 212 | + fi |
| 213 | + fi |
| 214 | +
|
| 215 | + # ── Releases page ───────────────────────────────────────────── |
| 216 | + cat > releases.md << RELEOF |
| 217 | + --- |
| 218 | + layout: default |
| 219 | + title: Releases |
| 220 | + permalink: /releases/ |
| 221 | + --- |
| 222 | + # Releases |
| 223 | + <div id="releases-container"><div class="loading">Loading releases...</div></div> |
| 224 | + <script> |
| 225 | + (async function(){ |
| 226 | + const c=document.getElementById('releases-container'); |
| 227 | + try{ |
| 228 | + const r=await fetch('https://api.github.com/repos/${REPO_FULL}/releases'); |
| 229 | + if(!r.ok)throw new Error(r.status); |
| 230 | + const data=await r.json(); |
| 231 | + if(!data.length){c.innerHTML='<p>No releases found.</p>';return} |
| 232 | + c.innerHTML=data.map((r,i)=>{ |
| 233 | + const d=new Date(r.published_at).toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric'}); |
| 234 | + const l=i===0?'<span class="release-latest">latest</span>':''; |
| 235 | + const a=r.assets.length?'<details class="release-assets"><summary>Assets ('+r.assets.length+')</summary>'+r.assets.map(a=>'<div class="release-asset">'+a.name+' <span style="color:#8b949e">'+(a.size/1024).toFixed(1)+' KB</span></div>').join('')+'</details>':''; |
| 236 | + let b=(r.body||'').replace(/^### (.+)$/gm,'<h3>$1</h3>').replace(/^## (.+)$/gm,'<h2>$1</h2>').replace(/^\* (.+)$/gm,'<li>$1</li>').replace(/(<li>.*<\/li>\n?)+/gs,'<ul>$&</ul>').replace(/\x60\x60\x60(\w*)\n([\s\S]*?)\x60\x60\x60/g,'<pre><code>$2</code></pre>').replace(/\x60([^\x60]+)\x60/g,'<code>$1</code>').replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2">$1</a>').replace(/\n\n/g,'<br><br>').replace(/\n/g,'<br>'); |
| 237 | + return '<div class="release"><div><span class="release-tag">'+r.tag_name+'</span>'+l+'<span class="release-date">'+d+'</span></div><div class="release-body">'+b+'</div>'+a+'</div>'; |
| 238 | + }).join(''); |
| 239 | + }catch(e){ |
| 240 | + c.innerHTML='<p>Unable to load releases. Visit <a href="https://github.com/${REPO_FULL}/releases">GitHub</a> directly.</p>'; |
| 241 | + } |
| 242 | + })(); |
| 243 | + </script> |
| 244 | + RELEOF |
| 245 | + echo "Created releases.md" |
| 246 | +
|
| 247 | + # ── License page (from LICENSE/LICENSE.txt) ──────────────────── |
| 248 | + LICENSE_SRC="" |
| 249 | + for lf in LICENSE LICENSE.txt; do |
| 250 | + [ -f "$lf" ] && LICENSE_SRC="$lf" && break |
| 251 | + done |
| 252 | +
|
| 253 | + if [ -n "$LICENSE_SRC" ] || [ -f "LICENSE.md" ]; then |
| 254 | + SRC="${LICENSE_SRC:-LICENSE.md}" |
| 255 | + LICENSE_TYPE="License" |
| 256 | + grep -qi "apache" "$SRC" 2>/dev/null && LICENSE_TYPE="Apache License 2.0" |
| 257 | + grep -qi "mit license" "$SRC" 2>/dev/null && LICENSE_TYPE="MIT License" |
| 258 | + grep -qi "gnu general public" "$SRC" 2>/dev/null && LICENSE_TYPE="GPL" |
| 259 | + grep -qi "bsd" "$SRC" 2>/dev/null && LICENSE_TYPE="BSD License" |
| 260 | +
|
| 261 | + if ! ([ -f "LICENSE.md" ] && head -1 LICENSE.md | grep -q '^\-\-\-'); then |
| 262 | + cat > LICENSE.md << LICEOF |
| 263 | + --- |
| 264 | + layout: default |
| 265 | + title: License |
| 266 | + permalink: /license/ |
| 267 | + --- |
| 268 | + # License |
| 269 | + This project is licensed under the **${LICENSE_TYPE}**. |
| 270 | +
|
| 271 | + See the [LICENSE](https://github.com/${REPO_FULL}/blob/main/${SRC}) file for the full license text. |
| 272 | + LICEOF |
| 273 | + echo "Created LICENSE.md (${LICENSE_TYPE})" |
| 274 | + fi |
| 275 | + fi |
| 276 | +
|
| 277 | + # ── CONTRIBUTING.md ─────────────────────────────────────────── |
| 278 | + if [ -f "CONTRIBUTING.md" ] && ! head -1 CONTRIBUTING.md | grep -q '^\-\-\-'; then |
| 279 | + TEMP=$(mktemp) |
| 280 | + printf -- '---\nlayout: default\ntitle: Contributing\npermalink: /contributing/\n---\n\n' > "$TEMP" |
| 281 | + cat CONTRIBUTING.md >> "$TEMP" |
| 282 | + mv "$TEMP" CONTRIBUTING.md |
| 283 | + echo "Processed CONTRIBUTING.md" |
| 284 | + fi |
| 285 | +
|
| 286 | + # ── CODE_OF_CONDUCT.md ──────────────────────────────────────── |
| 287 | + if [ -f "CODE_OF_CONDUCT.md" ] && ! head -1 CODE_OF_CONDUCT.md | grep -q '^\-\-\-'; then |
| 288 | + TEMP=$(mktemp) |
| 289 | + printf -- '---\nlayout: default\ntitle: Code of Conduct\npermalink: /code-of-conduct/\n---\n\n' > "$TEMP" |
| 290 | + cat CODE_OF_CONDUCT.md >> "$TEMP" |
| 291 | + mv "$TEMP" CODE_OF_CONDUCT.md |
| 292 | + echo "Processed CODE_OF_CONDUCT.md" |
| 293 | + fi |
| 294 | +
|
| 295 | + echo "::endgroup::" |
| 296 | +
|
| 297 | + echo "Final site source contents:" |
| 298 | + find . -type f | sort |
| 299 | +
|
| 300 | + - name: Setup Pages |
| 301 | + uses: actions/configure-pages@v5 |
| 302 | + |
| 303 | + - name: Build with Jekyll |
| 304 | + uses: actions/jekyll-build-pages@v1 |
| 305 | + with: |
| 306 | + source: ./_site_source |
| 307 | + destination: ./_site |
| 308 | + |
| 309 | + - name: Upload artifact |
| 310 | + uses: actions/upload-pages-artifact@v3 |
| 311 | + |
| 312 | + deploy: |
| 313 | + environment: |
| 314 | + name: github-pages |
| 315 | + url: ${{ steps.deployment.outputs.page_url }} |
| 316 | + runs-on: ubuntu-latest |
| 317 | + needs: build |
| 318 | + steps: |
| 319 | + - name: Deploy to GitHub Pages |
| 320 | + id: deployment |
| 321 | + uses: actions/deploy-pages@v5 |
0 commit comments