Publish site (main + drafts overlay) #45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Publish site (main + drafts overlay) | |
on: | |
workflow_dispatch: | |
inputs: | |
ref: | |
description: "Base ref to stage (usually main)" | |
required: false | |
default: "main" | |
mirror_branch: | |
description: "Also mirror staged source to the 'common-specs' branch" | |
type: boolean | |
required: false | |
default: true | |
force_deploy: | |
description: "Deploy even if no source diff detected" | |
type: boolean | |
required: false | |
default: false | |
push: | |
branches: [main] | |
paths: | |
- "_specs/**" | |
- "_layouts/**" | |
- "_includes/**" | |
- "_data/**" | |
- "assets/**" | |
- "_config.yml" | |
- "index.html" | |
- "spec.html" | |
- "style.css" | |
- ".github/workflows/**" | |
pull_request: | |
types: [opened, reopened, synchronize, labeled, unlabeled, closed] | |
paths: | |
- "_specs/**" | |
schedule: | |
- cron: "*/15 * * * *" | |
# needed to write branch (optional mirror) and to deploy Pages via Actions | |
permissions: | |
contents: write | |
pages: write | |
id-token: write | |
pull-requests: read | |
concurrency: | |
group: publish-common-specs | |
cancel-in-progress: true | |
jobs: | |
publish: | |
runs-on: ubuntu-latest | |
env: | |
BASE_REF: ${{ inputs.ref || 'main' }} | |
steps: | |
- name: Checkout base ref | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
ref: ${{ env.BASE_REF }} | |
- name: Stage current source into /tmp/site | |
run: | | |
set -e | |
rm -rf /tmp/site && mkdir -p /tmp/site | |
rsync -a --delete --exclude '.git' ./ /tmp/site/ | |
# We'll fill /tmp/site/_draft_specs next | |
rm -rf /tmp/site/_draft_specs | |
- name: Build _draft_specs overlay from open proposal PRs | |
id: build_drafts | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
const { execSync } = require('node:child_process'); | |
const fs = require('fs'); const path = require('path'); | |
const owner = context.repo.owner, repo = context.repo.repo; | |
// Collect OPEN PRs that look like proposals (or marked in-progress) | |
const prs = await github.paginate(github.rest.pulls.list, { | |
owner, repo, state: 'open', per_page: 100 | |
}); | |
let overlayCount = 0; | |
for (const pr of prs) { | |
const hasWanted = (pr.labels || []).some(l => | |
/^(spec:proposal|in-progress)$/i.test((l.name||'')) | |
); | |
if (!hasWanted) continue; | |
const files = await github.paginate(github.rest.pulls.listFiles, { | |
owner, repo, pull_number: pr.number, per_page: 100 | |
}); | |
const dirs = [...new Set( | |
files.map(f => f.filename) | |
.filter(p => p.startsWith('_specs/')) | |
.map(p => p.replace(/^(_specs\/[^/]+\/[^/]+)\/.*/, '$1')) | |
)]; | |
for (const dir of dirs) { | |
const m = dir.match(/^_specs\/([^/]+)\/([^/]+)/); | |
if (!m) continue; | |
const [_, family, version] = m; | |
const dst = path.join('/tmp/site/_draft_specs', family, version); | |
fs.mkdirSync(dst, { recursive: true }); | |
const list = execSync(`git ls-tree -r --name-only ${pr.head.sha} '${dir}'`, {encoding:'utf8'}) | |
.trim().split('\n').filter(Boolean); | |
for (const p of list) { | |
const rel = p.slice(dir.length + 1); | |
const out = path.join(dst, rel); | |
fs.mkdirSync(path.dirname(out), { recursive: true }); | |
const blob = execSync(`git show ${pr.head.sha}:'${p.replace(/'/g,"'\\''")}'`, {encoding:'utf8'}); | |
fs.writeFileSync(out, blob); | |
} | |
// inject PR metadata into the draft index.md (if present) | |
const idx = path.join(dst, 'index.md'); | |
if (fs.existsSync(idx)) { | |
const src = fs.readFileSync(idx, 'utf8'); | |
const parts = src.split(/^---\s*$/m); | |
if (parts.length >= 3) { | |
parts[1] += `\npr: ${pr.number}\npr_url: ${pr.html_url}\npr_updated_at: ${pr.updated_at}\n`; | |
fs.writeFileSync(idx, parts.join('---\n')); | |
} | |
} | |
overlayCount++; | |
} | |
} | |
core.setOutput('overlay_count', String(overlayCount)); | |
# Optional: keep an inspectable mirror of the staged source | |
- name: Mirror staged source to common-specs branch (create if missing) | |
if: ${{ inputs.mirror_branch != false }} | |
run: | | |
set -e | |
git fetch origin common-specs || true | |
rm -rf /tmp/common-specs | |
if git show-ref --verify --quiet refs/remotes/origin/common-specs; then | |
git worktree add -B common-specs /tmp/common-specs origin/common-specs | |
else | |
git worktree add -B common-specs /tmp/common-specs | |
fi | |
rsync -a --delete /tmp/site/ /tmp/common-specs/ | |
cd /tmp/common-specs | |
git config user.name "spec-bot" | |
git config user.email "[email protected]" | |
if ! git diff --quiet; then | |
git add -A | |
git commit -m "Mirror staged source (overlays=${{ steps.build_drafts.outputs.overlay_count || '0' }}) from ${BASE_REF}" | |
git push origin common-specs | |
else | |
echo "Mirror: no changes." | |
fi | |
# ---------- Build Jekyll locally ---------- | |
- name: Set up Ruby for Jekyll | |
uses: ruby/setup-ruby@v1 | |
with: | |
ruby-version: '3.2' | |
bundler-cache: true | |
working-directory: /tmp/site | |
- name: Install Jekyll (if no Gemfile) | |
run: | | |
set -e | |
cd /tmp/site | |
if [ ! -f Gemfile ]; then | |
gem install jekyll -N | |
fi | |
- name: Build Jekyll site | |
env: | |
JEKYLL_ENV: production | |
run: | | |
set -e | |
cd /tmp/site | |
if [ -f Gemfile ]; then | |
bundle exec jekyll build --trace | |
else | |
jekyll build --trace | |
fi | |
test -d /tmp/site/_site | |
# ---------- Deploy to GitHub Pages via Actions ---------- | |
- name: Configure Pages | |
uses: actions/configure-pages@v5 | |
- name: Upload site artifact | |
uses: actions/upload-pages-artifact@v3 | |
with: | |
path: /tmp/site/_site | |
- name: Deploy to Pages | |
id: deploy | |
uses: actions/deploy-pages@v4 | |
with: | |
# redeploy if forced, otherwise default behavior is fine | |
preview: ${{ inputs.force_deploy }} | |
- name: Done | |
run: | | |
echo "Deployed to: ${{ steps.deploy.outputs.page_url }}" |