Skip to content
Open
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
9 changes: 5 additions & 4 deletions .github/workflows/nightly-seed-grouping.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ jobs:
strategy:
fail-fast: false # Let all tests run and cascade errors, will only PR generator updates that passed
matrix:
sdk-name: [
sdk-name:
[
ruby-model,
ruby-sdk,
ruby-sdk-v2,
pydantic,
# python-sdk, Turned off until Python is no longer hangings
python-sdk,
fastapi,
openapi,
postman,
Expand Down Expand Up @@ -94,7 +95,7 @@ jobs:
build-seed-groups:
runs-on: ubuntu-latest
timeout-minutes: 3
if: always() && (needs.run-seed-test.result == 'success' || needs.run-seed-test.result == 'failure')
if: always() && !cancelled() && (needs.run-seed-test.result == 'success' || needs.run-seed-test.result == 'failure')
needs: [run-seed-test]
strategy:
fail-fast: false
Expand Down Expand Up @@ -171,7 +172,7 @@ jobs:
pr-seed-group-files:
runs-on: ubuntu-latest
timeout-minutes: 10
if: always() && (needs.build-seed-groups.result == 'success' || needs.build-seed-groups.result == 'failure')
if: always() && !cancelled() && (needs.build-seed-groups.result == 'success' || needs.build-seed-groups.result == 'failure')
needs: [build-seed-groups]
steps:
- name: Checkout Repo
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/update-seed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ jobs:
commit-seed-changes-by-push:
if: >-
${{
always() && needs.setup.outputs.update-by-push == 'true' &&
always() && !cancelled() && needs.setup.outputs.update-by-push == 'true' &&
!contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled')
}}
needs:
Expand Down Expand Up @@ -1147,7 +1147,7 @@ jobs:
commit-seed-changes-by-pr:
if: >-
${{
always() &&
always() && !cancelled() &&
github.ref == 'refs/heads/main' &&
needs.setup.outputs.update-by-pr == 'true' &&
!contains(needs.*.result, 'failure') &&
Expand Down
1 change: 1 addition & 0 deletions packages/seed/fern/definition/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ types:
fixtures:
type: optional<map<string, list<FixtureConfigurations>>>
scripts: optional<list<DockerScriptConfig>>
postScripts: optional<list<DockerScriptConfig>>
allowedFailures:
type: optional<list<string>>
docs: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,62 @@ export class DockerScriptRunner extends ScriptRunner {
return { type: "success" };
}

public async cleanup({
taskContext,
id,
outputDir
}: {
taskContext: TaskContext;
id: string;
outputDir?: AbsoluteFilePath;
}): Promise<void> {
if (this.skipScripts) {
return;
}

await this.startContainersFn;

const postScripts = this.workspace.workspaceConfig.postScripts ?? [];

if (postScripts.length > 0 && this.scripts.length > 0) {
// Run configured postScripts using existing script containers (avoids setup overhead)
for (let i = 0; i < postScripts.length && i < this.scripts.length; i++) {
const postScript = postScripts[i];
const containerToUse = this.scripts[i];

if (!postScript || !containerToUse) {
taskContext.logger.warn(`Skipping postScript ${i}: missing postScript or container`);
continue;
}

taskContext.logger.debug(
`Running postScript cleanup for fixture ${id} in container ${containerToUse.containerId}`
);

// Execute postScript commands directly without file copying overhead
const postScriptCommands = postScript.commands.join(" && ");

const cleanupCommand = await loggingExeca(
taskContext.logger,
"docker",
["exec", containerToUse.containerId, "/bin/sh", "-c", postScriptCommands],
{
doNotPipeOutput: false,
reject: false // Don't fail if cleanup has issues
}
);

if (cleanupCommand.failed) {
taskContext.logger.warn(`PostScript failed for fixture ${id}: ${cleanupCommand.stderr}`);
} else {
taskContext.logger.debug(`PostScript completed for fixture ${id}`);
}
}
}
}

public async stop(): Promise<void> {
// Stop script containers (postScripts reuse these containers)
for (const script of this.scripts) {
await loggingExeca(this.context.logger, "docker", ["kill", script.containerId], {
doNotPipeOutput: false
Expand Down Expand Up @@ -75,10 +130,11 @@ export class DockerScriptRunner extends ScriptRunner {
await writeFile(scriptFile.path, ["set -e", `cd /${workDir}/generated`, ...script.commands].join("\n"));

// Move scripts and generated files into the container
// Use mkdir -p to avoid failing if directory already exists (e.g., during cleanup)
const mkdirCommand = await loggingExeca(
taskContext.logger,
"docker",
["exec", containerId, "mkdir", `/${workDir}`],
["exec", containerId, "mkdir", "-p", `/${workDir}`],
{
doNotPipeOutput: false,
reject: false
Expand Down Expand Up @@ -150,7 +206,9 @@ export class DockerScriptRunner extends ScriptRunner {
private async startContainers(context: TaskContext): Promise<void> {
const absoluteFilePathToFernCli = await this.buildFernCli(context);
const cliVolumeBind = `${absoluteFilePathToFernCli}:/fern`;

// Start running a docker container for each script instance
// Note: postScripts reuse existing script containers
for (const script of this.workspace.workspaceConfig.scripts ?? []) {
const startSeedCommand = await loggingExeca(
context.logger,
Expand Down
35 changes: 35 additions & 0 deletions packages/seed/src/commands/test/script-runner/LocalScriptRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,41 @@ export class LocalScriptRunner extends ScriptRunner {
// No containers to stop for local execution
}

public async cleanup({
taskContext,
id,
outputDir
}: {
taskContext: TaskContext;
id: string;
outputDir?: AbsoluteFilePath;
}): Promise<void> {
if (this.skipScripts) {
return;
}

taskContext.logger.debug(`Cleaning up fixture ${id} (local execution)`);

const postScripts = this.workspace.workspaceConfig.postScripts ?? [];

if (postScripts.length > 0) {
// Run configured postScripts
for (const script of postScripts) {
const result = await this.runScript({
taskContext,
script,
id,
outputDir: outputDir ?? AbsoluteFilePath.of(process.cwd())
});
if (result.type === "failure") {
taskContext.logger.warn(
`PostScript failed for fixture ${id}: ${result.message ?? "Unknown error"}`
);
}
}
}
}

protected async initialize(): Promise<void> {
// No initialization needed for local execution
}
Expand Down
9 changes: 9 additions & 0 deletions packages/seed/src/commands/test/script-runner/ScriptRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export abstract class ScriptRunner {

public abstract run({ taskContext, id, outputDir }: ScriptRunner.RunArgs): Promise<ScriptRunner.RunResponse>;
public abstract stop(): Promise<void>;
public abstract cleanup({
taskContext,
id,
outputDir
}: {
taskContext: TaskContext;
id: string;
outputDir?: AbsoluteFilePath;
}): Promise<void>;

protected abstract initialize(): Promise<void>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface SeedWorkspaceConfiguration {
customFixtureConfig?: FernSeedConfig.FixtureConfigurations;
fixtures?: Record<string, FernSeedConfig.FixtureConfigurations[]>;
scripts?: FernSeedConfig.DockerScriptConfig[];
postScripts?: FernSeedConfig.DockerScriptConfig[];
/**
* List any fixtures that are okay to fail. For normal fixtures
* just list the fixture name. For configured fixture list {fixture}:{outputFolder}.
Expand Down
8 changes: 8 additions & 0 deletions seed/python-sdk/seed.yml
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,14 @@ scripts:
- poetry install
- poetry run mypy ./src ./tests
- poetry run pytest -rP .
postScripts:
# Clean up virtual environments and Poetry cache after fixture tests
- docker: fernapi/python-seed
commands:
- "find . -name '.venv*' -type d -exec rm -rf {} + 2>/dev/null || true"
- "rm -rf ./*-py* 2>/dev/null || true"
- "echo 'Virtual environment cleanup completed'"

allowedFailures:
- any-auth
- enum:real-enum # NOTE(tjb9dc): Failing due to format due to unescaped strings in the enum values
Expand Down
Loading