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
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function makeInitResult(overrides: Partial<ReplayInitResult> = {}): ReplayInitRe
bootstrap: makeBootstrap(null),
lockfileContent: undefined,
fernignoreEntries: [],
gitattributesEntries: [".fern/replay.lock linguist-generated=true"],
prBody: "",
...overrides
};
Expand Down Expand Up @@ -133,17 +134,23 @@ describe("InitCommand", () => {
})
);

global.fetch = vi.fn().mockResolvedValue({
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({ prUrl: "https://github.com/owner/repo/pull/1" })
}) as unknown as typeof fetch;
});
global.fetch = fetchMock as unknown as typeof fetch;

const context = createMockContext();
await cmd.handle(context, baseArgs());

expect(context.stderr.info).toHaveBeenCalledWith(
expect.stringContaining("https://github.com/owner/repo/pull/1")
);

expect(fetchMock).toHaveBeenCalledTimes(1);
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit];
const body = JSON.parse(init.body as string) as Record<string, unknown>;
expect(body.gitattributesEntries).toEqual([".fern/replay.lock linguist-generated=true"]);
});

it("throws CliError when response is missing prUrl", async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Send `gitattributesEntries` to Fiddle on `fern replay init` so the
generated PR marks `.fern/replay.lock` as `linguist-generated=true`.
Previously the entries were written into a temp clone that was discarded
and never reached the server. Requires a matching Fiddle server change
to consume the new field.
type: fix
1 change: 1 addition & 0 deletions packages/cli/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2965,6 +2965,7 @@ function addReplayInitCommand(cli: Argv<GlobalCliOptions>, cliContext: CliContex
repo,
lockfileContents: result.lockfileContent,
fernignoreEntries: result.fernignoreEntries,
gitattributesEntries: result.gitattributesEntries,
prBody: result.prBody
})
});
Expand Down
6 changes: 5 additions & 1 deletion packages/generator-cli/src/__test__/replay-pure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
logReplaySummary,
patchDescription
} from "../pipeline/replay-summary";
import { ensureReplayFernignoreEntries, REPLAY_FERNIGNORE_ENTRIES } from "../replay/fernignore";
import { ensureReplayFernignoreEntries, GITATTRIBUTES_ENTRIES, REPLAY_FERNIGNORE_ENTRIES } from "../replay/fernignore";

// ---------------------------------------------------------------------------
// formatConflictReason
Expand Down Expand Up @@ -438,6 +438,10 @@ describe("fernignore", () => {
expect(REPLAY_FERNIGNORE_ENTRIES.length).toBeGreaterThanOrEqual(3);
});

it("GITATTRIBUTES_ENTRIES marks the replay lockfile as linguist-generated", () => {
expect(GITATTRIBUTES_ENTRIES).toContain(".fern/replay.lock linguist-generated=true");
});

it("creates .fernignore with entries when no file exists, returns true", async () => {
const dir = await tmp.dir({ unsafeCleanup: true });
try {
Expand Down
2 changes: 1 addition & 1 deletion packages/generator-cli/src/replay/fernignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { join } from "path";

export const REPLAY_FERNIGNORE_ENTRIES = [".fern/replay.lock", ".fern/replay.yml", ".gitattributes"];

const GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];
export const GITATTRIBUTES_ENTRIES = [".fern/replay.lock linguist-generated=true"];

export async function ensureReplayFernignoreEntries(outputDir: string): Promise<boolean> {
const fernignorePath = join(outputDir, ".fernignore");
Expand Down
29 changes: 9 additions & 20 deletions packages/generator-cli/src/replay/replay-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import { type BootstrapResult, bootstrap } from "@fern-api/replay";
import { existsSync, readFileSync } from "fs";
import { join } from "path";
import tmp from "tmp-promise";
import {
ensureGitattributesEntriesSync,
ensureReplayFernignoreEntriesSync,
REPLAY_FERNIGNORE_ENTRIES
} from "./fernignore";
import { GITATTRIBUTES_ENTRIES, REPLAY_FERNIGNORE_ENTRIES } from "./fernignore";

export interface ReplayInitParams {
/** GitHub repo URI (e.g., "fern-demo/fern-replay-testbed-java-sdk") */
Expand All @@ -29,10 +25,10 @@ export interface ReplayInitResult {
bootstrap: BootstrapResult;
/** Raw lockfile YAML content, present when bootstrap succeeded and not dry-run */
lockfileContent?: string;
/** Raw replay.yml content, present only when fernignore migration created it */
replayYmlContent?: string;
/** Fernignore entries that the server should ensure exist */
fernignoreEntries: string[];
/** Gitattributes entries the server should ensure exist (e.g. linguist-generated markers) */
gitattributesEntries: string[];
/** Generated PR body markdown for the server to use */
prBody?: string;
}
Expand All @@ -46,8 +42,8 @@ export interface ReplayInitResult {
* Flow:
* 1. Clone the SDK repo (read-only)
* 2. Run bootstrap() to scan history and create lockfile
* 3. Ensure .fernignore has replay entries
* 4. Read lockfile content from disk and return it
* 3. Read lockfile content from disk
* 4. Return lockfile + fernignore/gitattributes entries for Fiddle to apply server-side
*/
export async function replayInit(params: ReplayInitParams): Promise<ReplayInitResult> {
const { githubRepo, token, dryRun } = params;
Expand All @@ -71,29 +67,22 @@ export async function replayInit(params: ReplayInitParams): Promise<ReplayInitRe
});

if (!bootstrapResult.generationCommit) {
return { bootstrap: bootstrapResult, fernignoreEntries: [] };
return { bootstrap: bootstrapResult, fernignoreEntries: [], gitattributesEntries: [] };
}

if (dryRun) {
return { bootstrap: bootstrapResult, fernignoreEntries: [] };
return { bootstrap: bootstrapResult, fernignoreEntries: [], gitattributesEntries: [] };
}

// 3. Ensure .fernignore has replay entries and .gitattributes marks lockfile as generated
ensureReplayFernignoreEntriesSync(repoPath);
ensureGitattributesEntriesSync(repoPath);

// 4. Read lockfile content from disk
// 3. Read lockfile content from disk
const lockfilePath = join(repoPath, ".fern", "replay.lock");
const lockfileContent = existsSync(lockfilePath) ? readFileSync(lockfilePath, "utf-8") : undefined;

const replayYmlPath = join(repoPath, ".fern", "replay.yml");
const replayYmlContent = existsSync(replayYmlPath) ? readFileSync(replayYmlPath, "utf-8") : undefined;

return {
bootstrap: bootstrapResult,
lockfileContent,
replayYmlContent,
fernignoreEntries: REPLAY_FERNIGNORE_ENTRIES,
gitattributesEntries: GITATTRIBUTES_ENTRIES,
prBody: buildPrBody(bootstrapResult)
};
}
Expand Down
1 change: 1 addition & 0 deletions packages/generator-cli/src/replay/replay-submit-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export async function submitReplayInit(params: SubmitReplayInitParams): Promise<
repo,
lockfileContents: initResult.lockfileContent,
fernignoreEntries: initResult.fernignoreEntries,
gitattributesEntries: initResult.gitattributesEntries,
prBody: initResult.prBody
})
});
Expand Down
14 changes: 14 additions & 0 deletions packages/generator-cli/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
# yaml-language-server: $schema=../../versions-yml.schema.json
- changelogEntry:
- summary: |
Send `gitattributesEntries` from `replayInit()` so Fiddle can mark
`.fern/replay.lock` as `linguist-generated=true` in the PR it builds.
Previously the local CLI wrote `.gitattributes` into a temp clone that
was discarded; the wire body never carried the entries, so generated
`fern replay init` PRs were missing the `.gitattributes` change. The
Fiddle server must consume this new field for the fix to take effect.
Also drops the unused `replayYmlContent` field and the temp-clone
writes that went nowhere.
type: fix
createdAt: "2026-04-28"
version: 0.9.19

- changelogEntry:
- summary: |
Stop pulling `@boundaryml/baml` into the static dependency graph of the
Expand Down