TS-Arena / scripts /fetch-news.mjs
mmcux's picture
skip posts marked draft: true
89b6a89
Raw
History Blame Contribute Delete
2.99 kB
#!/usr/bin/env node
/**
* Populates `content/news/` before `next build` (wired up as the `prebuild`
* npm script, so it runs on every build regardless of who invokes it).
*
* News posts are not committed to this repo — they come from a separate
* content repository, so an instance run by someone else does not inherit
* TS-Arena's announcements. Configure it with:
*
* NEWS_CONTENT_REPO clone URL of the content repo (unset = no news)
* NEWS_CONTENT_REF branch or tag to clone (default: main)
*
* For a private repo, put the access token in the URL. TS-Arena's own deploys
* are internal-only and anyone with Docker access on those hosts can read the
* content repo anyway, so the token ending up in the image is acceptable there.
*
* Deliberately lives in npm-script land rather than in the Dockerfile: the
* Coolify apps build with Nixpacks, which never reads the Dockerfile. Hooking
* into `prebuild` is the one place both build paths go through.
*/
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
const TARGET = path.join(process.cwd(), 'content', 'news');
const repo = process.env.NEWS_CONTENT_REPO?.trim();
const ref = process.env.NEWS_CONTENT_REF?.trim() || 'main';
if (!repo) {
// No content repo configured: leave whatever is there (local drafts) and
// make sure the directory exists so the loader has something to read.
fs.mkdirSync(TARGET, { recursive: true });
console.log('[news] NEWS_CONTENT_REPO not set — building without news posts');
process.exit(0);
}
console.log(`[news] cloning news content (ref: ${ref})`);
// Clone into a staging directory and only swap it in once it succeeds, so a
// failed fetch leaves whatever was already there untouched (locally, that is
// somebody's unpushed drafts).
const staging = `${TARGET}.tmp`;
fs.rmSync(staging, { recursive: true, force: true });
fs.mkdirSync(path.dirname(TARGET), { recursive: true });
try {
execFileSync('git', ['clone', '--depth', '1', '--branch', ref, repo, staging], {
stdio: ['ignore', 'inherit', 'pipe'],
// Bad credentials should fail the build, not hang it on a username prompt.
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
} catch (err) {
// Fail the build loudly. A silent fallback to "no news" would ship a site
// that quietly lost its announcements.
console.error(
`[news] failed to clone the news content repo at ref "${ref}". ` +
'Check NEWS_CONTENT_REPO / NEWS_CONTENT_REF and that git is available.',
);
console.error(err.stderr?.toString().trim() || err.message);
fs.rmSync(staging, { recursive: true, force: true });
process.exit(1);
}
fs.rmSync(path.join(staging, '.git'), { recursive: true, force: true });
fs.rmSync(TARGET, { recursive: true, force: true });
fs.renameSync(staging, TARGET);
const count = fs.readdirSync(TARGET).filter((f) => f.endsWith('.md')).length;
console.log(`[news] fetched ${count} post${count === 1 ? '' : 's'}`);