[Spec] sqlite wasm (scaffold) #66
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: | |
# when scaffold finishes | |
workflow_run: | |
workflows: ["Auto scaffold spec from issue"] # must equal the other workflow's `name:` | |
branches: [main] # only when that run's head_branch is main | |
types: [completed] | |
# Deploy the production site when main changes | |
push: | |
branches: [main] | |
paths: | |
- "_specs/**" | |
- "_layouts/**" | |
- "_includes/**" | |
- "_data/**" | |
- "assets/**" | |
- "_config.yml" | |
- "index.html" | |
- "spec.html" | |
- "style.css" | |
- ".github/workflows/**" | |
# Keep overlays fresh on PR lifecycle (labels at creation included) | |
pull_request_target: | |
branches: [main] # base branch = main | |
types: | |
- opened | |
- reopened | |
- synchronize | |
- ready_for_review | |
- labeled | |
- unlabeled | |
- closed | |
# Manual trigger | |
workflow_dispatch: | |
inputs: | |
ref: | |
description: "Base ref to stage (usually main)" | |
required: false | |
default: "main" | |
force_deploy: | |
description: "Deploy even if no source diff detected" | |
type: boolean | |
required: false | |
default: false | |
# Only what we need (no branch pushes) | |
permissions: | |
pages: write | |
id-token: write | |
concurrency: | |
group: publish-common-specs-${{ github.event.pull_request.number || 'main' }} | |
cancel-in-progress: true | |
jobs: | |
publish: | |
if: | | |
github.event_name != 'workflow_run' || | |
github.event.workflow_run.conclusion == 'success' | |
runs-on: ubuntu-latest | |
env: | |
BASE_REF: ${{ inputs.ref || 'main' }} | |
IS_PR: ${{ github.event_name == 'pull_request_target' }} | |
steps: | |
# -------- Early guard to avoid building on non-spec PRs -------- | |
- name: "Guard: allow push/dispatch, otherwise only spec PRs touching _specs/" | |
id: guard | |
uses: actions/github-script@v7 | |
with: | |
script: | | |
// Always OK for push/dispatch (we deploy from main there) | |
if (context.eventName !== 'pull_request_target') { | |
core.setOutput('ok', 'true'); | |
return; | |
} | |
const pr = context.payload.pull_request; | |
// Extra safety: only PRs into main | |
if ((pr.base?.ref || '') !== 'main') { | |
core.notice(`Skip: PR base is "${pr.base?.ref}", not "main".`); | |
core.setOutput('ok', 'false'); | |
return; | |
} | |
// Must have our spec labels | |
const hasLabel = (pr.labels || []).some(l => | |
/^(spec:proposal|in-progress)$/i.test((l.name || '')) | |
); | |
if (!hasLabel) { | |
core.notice('Skip: not a spec PR (labels missing).'); | |
core.setOutput('ok', 'false'); | |
return; | |
} | |
// Must actually change _specs/ | |
const files = await github.paginate( | |
github.rest.pulls.listFiles, | |
{ owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, per_page: 100 } | |
); | |
const touchesSpecs = files.some(f => f.filename.startsWith('_specs/')); | |
if (!touchesSpecs) { | |
core.notice('Skip: PR does not change _specs/.'); | |
core.setOutput('ok', 'false'); | |
return; | |
} | |
core.setOutput('ok', 'true'); | |
# -------- Stage the source (base = main or provided ref) -------- | |
- name: Checkout base ref | |
if: steps.guard.outputs.ok == 'true' | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 0 | |
ref: ${{ env.BASE_REF }} | |
- name: Stage current source into /tmp/site | |
if: steps.guard.outputs.ok == 'true' | |
run: | | |
set -e | |
rm -rf /tmp/site && mkdir -p /tmp/site | |
rsync -a --delete --exclude '.git' ./ /tmp/site/ | |
rm -rf /tmp/site/_draft_specs | |
# -------- Build _draft_specs overlay from open proposal/in-progress PRs -------- | |
- name: Build _draft_specs overlay from open proposal PRs | |
if: steps.guard.outputs.ok == 'true' | |
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 | |
const prs = await github.paginate(github.rest.pulls.list, { | |
owner, repo, state: 'open', per_page: 100 | |
}); | |
let overlayCount = 0; | |
for (const pr of prs) { | |
// Only overlay PRs with our labels | |
const hasWanted = (pr.labels || []).some(l => | |
/^(spec:proposal|in-progress)$/i.test((l.name || '')) | |
); | |
if (!hasWanted) continue; | |
// Read changed files to find spec dirs | |
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 }); | |
// List files from the PR head for that dir | |
const listRaw = execSync(`git ls-tree -r --name-only ${pr.head.sha} '${dir}'`, { encoding: 'utf8' }).trim(); | |
const list = listRaw ? listRaw.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 safe = p.replace(/'/g, "'\\''"); | |
const blob = execSync(`git show ${pr.head.sha}:'${safe}'`, { encoding: 'utf8' }); | |
fs.writeFileSync(out, blob); | |
} | |
// Inject PR metadata into 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)); | |
# -------- Build the Jekyll site locally -------- | |
- name: Set up Ruby for Jekyll | |
if: steps.guard.outputs.ok == 'true' | |
uses: ruby/setup-ruby@v1 | |
with: | |
ruby-version: '3.2' | |
bundler-cache: true | |
working-directory: /tmp/site | |
- name: Install Jekyll (if no Gemfile) | |
if: steps.guard.outputs.ok == 'true' | |
run: | | |
set -e | |
cd /tmp/site | |
if [ ! -f Gemfile ]; then | |
gem install jekyll -N | |
fi | |
- name: Build Jekyll site | |
if: steps.guard.outputs.ok == 'true' | |
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 (ONLY push to main or manual run) -------- | |
- name: Configure Pages | |
if: steps.guard.outputs.ok == 'true' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target') | |
uses: actions/configure-pages@v5 | |
- name: Upload site artifact | |
if: steps.guard.outputs.ok == 'true' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target') | |
uses: actions/upload-pages-artifact@v3 | |
with: | |
path: /tmp/site/_site | |
- name: Deploy to Pages | |
if: steps.guard.outputs.ok == 'true' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request_target') | |
id: deploy | |
uses: actions/deploy-pages@v4 | |
with: | |
# Preview for PRs; regular deploy for push/dispatch | |
preview: ${{ github.event_name == 'pull_request_target' || inputs.force_deploy }} |