diff --git a/.github/workflows/nightly-seed-grouping.yml b/.github/workflows/nightly-seed-grouping.yml index 82548e38005..b0a711dac5a 100644 --- a/.github/workflows/nightly-seed-grouping.yml +++ b/.github/workflows/nightly-seed-grouping.yml @@ -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, @@ -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 @@ -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 diff --git a/.github/workflows/update-seed.yml b/.github/workflows/update-seed.yml index 908a3bd7d28..5ca1bc8456c 100644 --- a/.github/workflows/update-seed.yml +++ b/.github/workflows/update-seed.yml @@ -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: @@ -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') && diff --git a/packages/seed/fern/definition/config.yml b/packages/seed/fern/definition/config.yml index 34c1a6221fc..5d20b0a580a 100644 --- a/packages/seed/fern/definition/config.yml +++ b/packages/seed/fern/definition/config.yml @@ -38,6 +38,7 @@ types: fixtures: type: optional>> scripts: optional> + postScripts: optional> allowedFailures: type: optional> docs: | diff --git a/packages/seed/src/commands/test/script-runner/DockerScriptRunner.ts b/packages/seed/src/commands/test/script-runner/DockerScriptRunner.ts index cee6fe0f43f..e42bd31ec72 100644 --- a/packages/seed/src/commands/test/script-runner/DockerScriptRunner.ts +++ b/packages/seed/src/commands/test/script-runner/DockerScriptRunner.ts @@ -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 { + 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 { + // Stop script containers (postScripts reuse these containers) for (const script of this.scripts) { await loggingExeca(this.context.logger, "docker", ["kill", script.containerId], { doNotPipeOutput: false @@ -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 @@ -150,7 +206,9 @@ export class DockerScriptRunner extends ScriptRunner { private async startContainers(context: TaskContext): Promise { 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, diff --git a/packages/seed/src/commands/test/script-runner/LocalScriptRunner.ts b/packages/seed/src/commands/test/script-runner/LocalScriptRunner.ts index 3693491f80f..75a879dda3d 100644 --- a/packages/seed/src/commands/test/script-runner/LocalScriptRunner.ts +++ b/packages/seed/src/commands/test/script-runner/LocalScriptRunner.ts @@ -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 { + 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 { // No initialization needed for local execution } diff --git a/packages/seed/src/commands/test/script-runner/ScriptRunner.ts b/packages/seed/src/commands/test/script-runner/ScriptRunner.ts index 0122ef0ae94..ca442a86408 100644 --- a/packages/seed/src/commands/test/script-runner/ScriptRunner.ts +++ b/packages/seed/src/commands/test/script-runner/ScriptRunner.ts @@ -37,6 +37,15 @@ export abstract class ScriptRunner { public abstract run({ taskContext, id, outputDir }: ScriptRunner.RunArgs): Promise; public abstract stop(): Promise; + public abstract cleanup({ + taskContext, + id, + outputDir + }: { + taskContext: TaskContext; + id: string; + outputDir?: AbsoluteFilePath; + }): Promise; protected abstract initialize(): Promise; } diff --git a/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts b/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts index fa29400753e..79d37c5a6f0 100644 --- a/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts +++ b/packages/seed/src/config/api/resources/config/types/SeedWorkspaceConfiguration.ts @@ -22,6 +22,7 @@ export interface SeedWorkspaceConfiguration { customFixtureConfig?: FernSeedConfig.FixtureConfigurations; fixtures?: Record; 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}. diff --git a/seed/python-sdk/seed.yml b/seed/python-sdk/seed.yml index 188850e3ccf..52d1a6c012e 100644 --- a/seed/python-sdk/seed.yml +++ b/seed/python-sdk/seed.yml @@ -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