fix: persist OG image cache outside act runner workspace
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m51s

The cache was written to .cache/og/ relative to the workspace, which is
under /usr/local/git/.cache/act/<unique-hash>/hostexecutor/ — a new path
per run, so every build regenerated all images from scratch.

OG_CACHE_DIR env var now controls the cache path (resolved to an absolute
path). CI sets it to /usr/local/git/.cache/og, which survives between runs.
Locally it still defaults to .cache/og inside the project dir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-04-01 10:00:46 +02:00
parent 06d8cab329
commit 93972aef35
4 changed files with 24 additions and 22 deletions

View File

@@ -125,6 +125,7 @@ jobs:
GITEA_URL: https://gitea.giersig.eu
GITEA_INTERNAL_URL: http://127.0.0.1:3000
GITEA_ORG: giersig.eu
OG_CACHE_DIR: /usr/local/git/.cache/og
- name: Deploy via rsync
run: |

View File

@@ -26,6 +26,12 @@ const postGraph = esmRequire("@rknightuk/eleventy-plugin-post-graph");
const __dirname = dirname(fileURLToPath(import.meta.url));
const siteUrl = process.env.SITE_URL || "https://example.com";
// OG image cache — persistent across CI runs when OG_CACHE_DIR env var is set.
// In CI, point this outside the act runner workspace (e.g. /usr/local/git/.cache/og).
const OG_CACHE_DIR = process.env.OG_CACHE_DIR
? resolve(process.env.OG_CACHE_DIR)
: resolve(__dirname, ".cache", "og");
// Slugify each path segment, preserving "/" separators for nested tags (e.g. "tech/programming")
const nestedSlugify = (str) => {
if (!str) return "";
@@ -49,7 +55,7 @@ export default function (eleventyConfig) {
eleventyConfig.setUseGitIgnore(false);
// Passthrough copy for OG images
eleventyConfig.addPassthroughCopy({ ".cache/og": "images/og" });
eleventyConfig.addPassthroughCopy({ [OG_CACHE_DIR]: "images/og" });
// Ignore output directory (prevents re-processing generated files via symlink)
eleventyConfig.ignores.add("_site");
@@ -78,8 +84,8 @@ export default function (eleventyConfig) {
eleventyConfig.watchIgnores.add("/app/data/site/**");
eleventyConfig.watchIgnores.add("pagefind");
eleventyConfig.watchIgnores.add("pagefind/**");
eleventyConfig.watchIgnores.add(".cache/og");
eleventyConfig.watchIgnores.add(".cache/og/**");
eleventyConfig.watchIgnores.add(OG_CACHE_DIR);
eleventyConfig.watchIgnores.add(OG_CACHE_DIR + "/**");
eleventyConfig.watchIgnores.add(".cache/unfurl");
eleventyConfig.watchIgnores.add(".cache/unfurl/**");
@@ -388,9 +394,8 @@ export default function (eleventyConfig) {
eleventyConfig.on("eleventy.before", () => { _ogFileSet = null; });
function hasOgImage(ogSlug) {
if (!_ogFileSet) {
const ogDir = resolve(__dirname, ".cache", "og");
try {
_ogFileSet = new Set(readdirSync(ogDir));
_ogFileSet = new Set(readdirSync(OG_CACHE_DIR));
} catch {
_ogFileSet = new Set();
}
@@ -658,7 +663,7 @@ export default function (eleventyConfig) {
eleventyConfig.addPassthroughCopy("favicon.ico");
eleventyConfig.addPassthroughCopy("robots.txt");
eleventyConfig.addPassthroughCopy("interactive");
eleventyConfig.addPassthroughCopy({ ".cache/og": "og" });
eleventyConfig.addPassthroughCopy({ [OG_CACHE_DIR]: "og" });
// Funkwhale images are copied in eleventy.after (after data files download them)
// Copy vendor web components from node_modules
@@ -929,8 +934,7 @@ export default function (eleventyConfig) {
// Check if a generated OG image exists for this slug
eleventyConfig.addFilter("hasOgImage", (slug) => {
if (!slug) return false;
const ogPath = resolve(__dirname, ".cache", "og", `${slug}.png`);
return existsSync(ogPath);
return existsSync(resolve(OG_CACHE_DIR, `${slug}.png`));
});
// Inline file contents (for critical CSS inlining)
@@ -1589,7 +1593,6 @@ export default function (eleventyConfig) {
eleventyConfig.on("eleventy.before", () => {
console.time("[og] image generation");
const contentDir = resolve(__dirname, "content");
const cacheDir = resolve(__dirname, ".cache");
const siteName = process.env.SITE_NAME || "My IndieWeb Blog";
const BATCH_SIZE = 100;
try {
@@ -1601,7 +1604,7 @@ export default function (eleventyConfig) {
"--expose-gc",
resolve(__dirname, "lib", "og-cli.js"),
contentDir,
cacheDir,
OG_CACHE_DIR,
siteName,
String(BATCH_SIZE),
], {
@@ -1620,16 +1623,15 @@ export default function (eleventyConfig) {
}
// Sync new OG images to output directory.
// During incremental builds, .cache/og is in watchIgnores so Eleventy's
// During incremental builds, OG_CACHE_DIR is in watchIgnores so Eleventy's
// passthrough copy won't pick up newly generated images. Copy them manually.
const ogCacheDir = resolve(cacheDir, "og");
const ogOutputDir = resolve(__dirname, "_site", "og");
if (existsSync(ogCacheDir) && existsSync(resolve(__dirname, "_site"))) {
if (existsSync(OG_CACHE_DIR) && existsSync(resolve(__dirname, "_site"))) {
mkdirSync(ogOutputDir, { recursive: true });
let synced = 0;
for (const file of readdirSync(ogCacheDir)) {
for (const file of readdirSync(OG_CACHE_DIR)) {
if (file.endsWith(".png") && !existsSync(resolve(ogOutputDir, file))) {
copyFileSync(resolve(ogCacheDir, file), resolve(ogOutputDir, file));
copyFileSync(resolve(OG_CACHE_DIR, file), resolve(ogOutputDir, file));
synced++;
}
}

View File

@@ -4,7 +4,7 @@
* CLI entry point for OG image generation.
* Runs as a separate process to isolate memory from Eleventy.
*
* Usage: node lib/og-cli.js <contentDir> <cacheDir> <siteName> [batchSize]
* Usage: node lib/og-cli.js <contentDir> <ogDir> <siteName> [batchSize]
*
* batchSize: Max images to generate per invocation (0 = unlimited).
* When set, exits after generating that many images so the caller
@@ -14,15 +14,15 @@
import { generateOgImages } from "./og.js";
const [contentDir, cacheDir, siteName, batchSizeStr] = process.argv.slice(2);
const [contentDir, ogDir, siteName, batchSizeStr] = process.argv.slice(2);
if (!contentDir || !cacheDir || !siteName) {
console.error("[og] Usage: node og-cli.js <contentDir> <cacheDir> <siteName> [batchSize]");
if (!contentDir || !ogDir || !siteName) {
console.error("[og] Usage: node og-cli.js <contentDir> <ogDir> <siteName> [batchSize]");
process.exit(1);
}
const batchSize = parseInt(batchSizeStr, 10) || 0;
const result = await generateOgImages(contentDir, cacheDir, siteName, batchSize);
const result = await generateOgImages(contentDir, ogDir, siteName, batchSize);
// Exit code 2 signals "batch complete, more images remain"
if (result?.hasMore) {

View File

@@ -426,8 +426,7 @@ function scanContentFiles(contentDir) {
* @param {number} batchSize - Max images to generate (0 = unlimited)
* @returns {{ hasMore: boolean }} Whether more images need generation
*/
export async function generateOgImages(contentDir, cacheDir, siteName, batchSize = 0) {
const ogDir = join(cacheDir, "og");
export async function generateOgImages(contentDir, ogDir, siteName, batchSize = 0) {
mkdirSync(ogDir, { recursive: true });
const manifestPath = join(ogDir, "manifest.json");