name: Deploy to WebDAV (/BLB) # Mirror the site content from the repo to the WebDAV directory served at /BLB/. # Runs on every push/merge to the default branch (main). Only the files that # actually changed in the pushed range are uploaded (efficient for a ~32k-file # static tree), using DELETE-then-PUT so the serving layer's cache is busted. "on": push: branches: - main jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy changed files to WebDAV env: WEBDAV_USER: ${{ secrets.WEBDAV_USER }} WEBDAV_PASS: ${{ secrets.WEBDAV_PASS }} WEBDAV_BASE: https://files.thecookiejar.me/www/static/BLB GITHUB_BEFORE: ${{ github.event.before }} GITHUB_SHA: ${{ github.sha }} run: | if [ -z "$WEBDAV_USER" ] || [ -z "$WEBDAV_PASS" ]; then echo "::error::WEBDAV_USER / WEBDAV_PASS secrets are not set on the exlim org." exit 1 fi node --input-type=commonjs - <<'NODE' const { execSync } = require('child_process'); const fs = require('fs'); (async () => { const USER = process.env.WEBDAV_USER; const PASS = process.env.WEBDAV_PASS; const BASE = process.env.WEBDAV_BASE; const BEFORE = process.env.GITHUB_BEFORE || ''; const SHA = process.env.GITHUB_SHA || ''; const vacant = '0'.repeat(40); // Simple content-type map (node:20 image has no `mime` package). const MIME = { html: 'text/html', htm: 'text/html', js: 'application/javascript', mjs: 'application/javascript', css: 'text/css', json: 'application/json', txt: 'text/plain', md: 'text/markdown', xml: 'application/xml', gif: 'image/gif', jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', ico: 'image/x-icon', svg: 'image/svg+xml', pdf: 'application/pdf', mid: 'audio/midi', mp3: 'audio/mpeg', wav: 'audio/x-wav', rm: 'application/vnd.rn-realmedia', swf: 'application/x-shockwave-flash', jsx: 'text/javascript', }; const typeFor = (f) => { const e = f.split('.').pop().toLowerCase(); return MIME[e] || 'application/octet-stream'; }; // Which files to deploy? On a normal push, only those changed in the // pushed range. On the very first deploy (empty before), every file. let files; if (BEFORE && BEFORE !== vacant) { files = execSync(`git diff-tree -r --name-only -z "${BEFORE}" "${SHA}"`, { encoding: 'utf8' }) .split('\0').filter(Boolean); } else { files = execSync('git ls-files -z', { encoding: 'utf8' }) .split('\0').filter(Boolean); } // Never treat CI/config files as site content. files = files.filter(f => !f.startsWith('.gitea') && !f.startsWith('.github')); if (files.length === 0) { console.log('No site files changed; nothing to deploy.'); return; } console.log(`Deploying ${files.length} changed file(s):`); for (const f of files.slice(0, 20)) console.log(' ' + f); if (files.length > 20) console.log(` ... and ${files.length - 20} more`); const auth = 'Basic ' + Buffer.from(`${USER}:${PASS}`).toString('base64'); const enc = (p) => p.split('/').map(encodeURIComponent).join('/'); const urlFor = (f) => BASE + '/' + enc(f); async function delThenPut(f) { const url = urlFor(f); const body = fs.readFileSync(f); // DELETE first: serving layer caches PUTs, so overwrite without // delete silently keeps serving the OLD copy. try { await fetch(url, { method: 'DELETE', headers: { Authorization: auth } }); } catch (e) { /* file may not exist yet -- fine */ } const r = await fetch(url, { method: 'PUT', headers: { Authorization: auth, 'Content-Type': typeFor(f) }, body, }); if (!r.ok) throw new Error(`PUT ${url} -> ${r.status} ${r.statusText}`); } // Small worker pool so large first deploys don't crush the server. const N = 8; let i = 0; const failures = []; async function worker() { while (i < files.length) { const f = files[i++]; try { await delThenPut(f); } catch (e) { failures.push(`${f}: ${e.message}`); console.log(`::warning::failed ${f} -- ${e.message}`); } } } await Promise.all(Array.from({ length: N }, worker)); if (failures.length) { console.log(`::error::${failures.length}/${files.length} files failed to deploy`); for (const f of failures.slice(0, 20)) console.log(' ' + f); process.exit(1); } console.log(`Deployed ${files.length} file(s) successfully.`); })().catch((e) => { console.error(e); process.exit(1); }); NODE