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_URL: https://gitea.giersig.eu
GITEA_INTERNAL_URL: http://127.0.0.1:3000 GITEA_INTERNAL_URL: http://127.0.0.1:3000
GITEA_ORG: giersig.eu GITEA_ORG: giersig.eu
OG_CACHE_DIR: /usr/local/git/.cache/og
- name: Deploy via rsync - name: Deploy via rsync
run: | run: |

View File

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

View File

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

View File

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