Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/content-indexer/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { config as dotenvConfig } from "dotenv";
import fs from "fs";
import path from "path";

import { uploadChangelogFile } from "@/content-indexer/uploaders/preview-changelog.ts";
import { uploadMdxFile } from "@/content-indexer/uploaders/preview-mdx.ts";
import { runIndexAndUpload } from "@/content-indexer/utils/preview-index.ts";
import {
runChangelogIndexAndUpload,
runIndexAndUpload,
} from "@/content-indexer/utils/preview-index.ts";
import { buildPreviewUrl } from "@/content-indexer/utils/preview-url.ts";
import { startWatchers } from "@/content-indexer/utils/preview-watchers.ts";
import { getRedis } from "@/content-indexer/utils/redis.ts";
Expand Down Expand Up @@ -87,6 +91,23 @@ const main = async () => {
if (uploadFile) {
const redis = getRedis();
const filePath = uploadFile.replace(/^fern\//, "");

// Changelog files: different key prefix + reindex logic
if (filePath.startsWith("changelog/") && filePath.endsWith(".md")) {
const filename = path.basename(filePath);
const { reindexNeeded } = await uploadChangelogFile(
filename,
branch,
redis,
);
if (reindexNeeded) {
console.info(" 🔄 New/deleted changelog file, re-indexing...");
await runChangelogIndexAndUpload(branch);
}
return;
}

// MDX files (existing behavior)
const { reindexNeeded } = await uploadMdxFile(filePath, branch, redis);

if (reindexNeeded) {
Expand Down
120 changes: 120 additions & 0 deletions src/content-indexer/uploaders/preview-changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type { Redis } from "@upstash/redis";
import { execSync } from "child_process";
import fs from "fs/promises";
import path from "path";

import type { PathIndex } from "@/content-indexer/types/pathIndex.ts";

import { PREVIEW_TTL_SECONDS } from "./redis.ts";

/**
* Uploads a single changelog file to Redis under a branch-scoped key.
* Returns whether a reindex is needed (new or deleted file).
*
* @param filename - Changelog filename (e.g., "2025-11-20.md")
* @param branch - Branch identifier for Redis key prefix
* @param redis - Redis client instance
*/
export const uploadChangelogFile = async (
filename: string,
branch: string,
redis: Redis,
): Promise<{ reindexNeeded: boolean }> => {
const fullPath = path.join(process.cwd(), "fern", "changelog", filename);
const redisKey = `${branch}:changelog:${filename}`;

let content: string;
try {
content = await fs.readFile(fullPath, "utf-8");
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
// File deleted — remove branch key so previewGet falls back to main:
await redis.del(redisKey);
console.info(` 🗑️ ${filename} deleted -> removed ${redisKey}`);
return { reindexNeeded: true };
}
throw error;
}

// Detect new files: no existing branch key or main key means this is new
const existingBranchContent = await redis.get<string>(redisKey);
const existingMainContent = await redis.get<string>(
`main:changelog:${filename}`,
);
const isNew = existingBranchContent === null && existingMainContent === null;

await redis.set(redisKey, content, { ex: PREVIEW_TTL_SECONDS });
console.info(` 📄 ${filename} -> ${redisKey}`);

// New files need reindex (to add the route to the index); content-only edits don't
// because the changelog index only stores date + filePath, not content.
return { reindexNeeded: isNew };
};

/**
* Returns changelog filenames that differ from main.
* Includes both committed changes (git diff) and untracked new files.
*/
const getChangedChangelogFiles = (): string[] => {
// Committed/staged changes vs main
const diffOutput = execSync(
"git diff --name-only origin/main -- fern/changelog/",
{ encoding: "utf-8" },
);

// Untracked new files not yet committed
const untrackedOutput = execSync(
"git ls-files --others --exclude-standard -- fern/changelog/",
{ encoding: "utf-8" },
);

const allFiles = new Set([
...diffOutput.trim().split("\n"),
...untrackedOutput.trim().split("\n"),
]);

return [...allFiles]
.filter((line) => line.length > 0 && line.endsWith(".md"))
.map((line) => path.basename(line));
};

/**
* Uploads only changelog files that differ from main and exist in the path index.
*
* @param pathIndex - The changelog path index to validate against
* @param branch - Branch identifier for Redis key prefix
* @param redis - Redis client instance
*/
export const uploadChangedChangelogFiles = async (
pathIndex: PathIndex,
branch: string,
redis: Redis,
): Promise<void> => {
const changedFiles = getChangedChangelogFiles();

// Only upload files that are in the path index
const indexedFiles = new Set(
Object.values(pathIndex)
.filter((entry) => entry.type === "changelog")
.map((entry) => entry.filePath),
);

const toUpload = changedFiles.filter((f) => indexedFiles.has(f));

if (toUpload.length === 0) {
console.info("\n📤 No changed changelog files to upload");
return;
Comment thread
dslovinsky marked this conversation as resolved.
}

console.info(
`\n📤 Uploading ${toUpload.length} changed changelog file${toUpload.length === 1 ? "" : "s"} to Redis...`,
);

await Promise.all(
toUpload.map((filename) => uploadChangelogFile(filename, branch, redis)),
);

console.info(
`✅ ${toUpload.length} changelog file${toUpload.length === 1 ? "" : "s"} uploaded`,
);
};
29 changes: 29 additions & 0 deletions src/content-indexer/utils/preview-index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import path from "path";

import { buildChangelogIndex } from "@/content-indexer/indexers/changelog.ts";
import { buildDocsContentIndex } from "@/content-indexer/indexers/main.ts";
import { uploadChangedChangelogFiles } from "@/content-indexer/uploaders/preview-changelog.ts";
import { uploadChangedMdxFiles } from "@/content-indexer/uploaders/preview-mdx.ts";
import { uploadSpecs } from "@/content-indexer/uploaders/preview-specs.ts";
import { storeToRedis } from "@/content-indexer/uploaders/redis.ts";
Expand Down Expand Up @@ -38,4 +40,31 @@ export const runIndexAndUpload = async (branch: string): Promise<void> => {
? uploadSpecs(specs, branch, redis)
: Promise.resolve(),
]);

await runChangelogIndexAndUpload(branch);
};

/**
* Runs the changelog indexer in preview mode and uploads changed changelog files.
* Stores the changelog path index under a branch-scoped Redis key,
* then uploads only changelog files that differ from main.
*/
export const runChangelogIndexAndUpload = async (
branch: string,
): Promise<void> => {
console.info("\n🔍 Running changelog indexer (preview mode)...\n");

const { pathIndex } = await buildChangelogIndex({
localBasePath: path.join(process.cwd(), "fern/changelog"),
branchId: branch,
});

await storeToRedis(pathIndex, undefined, {
branchId: branch,
indexerType: "changelog",
quiet: true,
});

const redis = getRedis();
await uploadChangedChangelogFiles(pathIndex, branch, redis);
};
Loading