Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5e5a92e
fix(ci): use GITHUB_TOKEN for Dependabot alerts instead of FERN_GITHU…
davidkonigsberg Jun 15, 2026
5e9d2e8
fix(php): emit required variable-typed path parameters in dynamic sni…
iamnamananand996 Jun 15, 2026
fb89a94
chore(php): release 2.10.12
github-actions[bot] Jun 15, 2026
31da9d4
fix(deps): bump esbuild to 0.28.1 to fix GHSA-gv7w-rqvm-qjhr, GHSA-2q…
github-actions[bot] Jun 15, 2026
5c8f099
chore(typescript): release 3.72.3
github-actions[bot] Jun 15, 2026
fc5e53b
fix(docs): use index.mdx for library autodocs section overviews (#16120)
Ryan-Amirthan Jun 15, 2026
d443dd1
chore(cli): release 5.49.1
github-actions[bot] Jun 15, 2026
f53c254
fix(python): fix union base properties in snippets and conftest heade…
iamnamananand996 Jun 15, 2026
1245ade
chore(python): release 5.14.16
github-actions[bot] Jun 15, 2026
c61f6d7
fix(generator-cli): omit author so GitHub auto-signs API-created comm…
tstanmay13 Jun 15, 2026
a893ff7
fix(go): support pagination results that are named aliases to a list/…
iamnamananand996 Jun 15, 2026
54d1158
chore(go): release 1.45.3
github-actions[bot] Jun 15, 2026
1a5b41d
fix(go): serialize undiscriminated union request headers via String()…
iamnamananand996 Jun 15, 2026
19821cf
chore(go): release 1.45.4
github-actions[bot] Jun 15, 2026
d56c6a9
fix(python): fix OAuth token provider for custom parameter names (#16…
iamnamananand996 Jun 15, 2026
c236bc4
chore(python): release 5.14.17
github-actions[bot] Jun 15, 2026
873c100
fix(php): forward required custom OAuth token request properties (#16…
iamnamananand996 Jun 15, 2026
ea3192b
chore(php): release 2.10.13
github-actions[bot] Jun 15, 2026
500bf45
fix(python): remove examples:legacy-wire-tests from allowedFailures (…
iamnamananand996 Jun 15, 2026
a294080
chore(python): release 5.14.18
github-actions[bot] Jun 15, 2026
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
15 changes: 7 additions & 8 deletions .github/workflows/security-scanning-and-remediation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ name: Create PRs to Remediate vulns
# creates a PR if actionable vulnerabilities are found.
#
# TOKEN REQUIREMENTS:
# - FERN_GITHUB_TOKEN: PAT with `security_events` scope for Dependabot alerts
# - GITHUB_TOKEN: Default token for creating branches and PRs
# - GITHUB_TOKEN: Default token with vulnerability-alerts:read for Dependabot alerts,
# and contents:write for creating branches and PRs
# - DEVIN_AI_PR_BOT_SLACK_TOKEN: For Slack notifications to Devin AI
# ============================================================

Expand Down Expand Up @@ -55,14 +55,13 @@ jobs:
env:
ALERTS_FILE: ${{ runner.temp }}/dependabot-alerts.json
with:
github-token: ${{ secrets.FERN_GITHUB_TOKEN }}
script: |
const fs = require('fs');
const owner = context.repo.owner;
const repo = context.repo.repo;
const alertsFile = process.env.ALERTS_FILE;

// Fetch all open Dependabot alerts using FERN_GITHUB_TOKEN
// Fetch all open Dependabot alerts using the workflow's GITHUB_TOKEN
let alerts = [];
try {
const response = await github.rest.dependabot.listAlertsForRepo({
Expand All @@ -73,12 +72,12 @@ jobs:
});
alerts = response.data;
} catch (error) {
if (error.status === 403) {
if (error.status === 403 || error.status === 404) {
console.log('ERROR: Unable to access Dependabot alerts. This is likely because:');
console.log('1. The FERN_GITHUB_TOKEN secret is not set, OR');
console.log('2. The token does not have the required `security_events` scope');
console.log('1. Dependabot alerts are not enabled for this repository, OR');
console.log('2. The GITHUB_TOKEN does not have the required `vulnerability-alerts: read` permission');
console.log('');
console.log('To fix: Ensure FERN_GITHUB_TOKEN is a PAT (classic) with `security_events` scope');
console.log('To fix: Enable Dependabot alerts in repo settings or ensure the workflow has appropriate permissions');
fs.writeFileSync(alertsFile, '[]');
core.setOutput('alerts_count', '0');
return;
Expand Down
45 changes: 45 additions & 0 deletions generators/go-v2/base/src/context/AbstractGoGeneratorContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,51 @@ export abstract class AbstractGoGeneratorContext<
}
}

public maybeUndiscriminatedUnion(
typeReference: FernIr.TypeReference
): FernIr.UndiscriminatedUnionTypeDeclaration | undefined {
switch (typeReference.type) {
case "named": {
const declaration = this.getTypeDeclarationOrThrow(typeReference.typeId);
switch (declaration.shape.type) {
case "undiscriminatedUnion":
return declaration.shape;
case "alias":
return this.maybeUndiscriminatedUnion(declaration.shape.aliasOf);
case "enum":
case "object":
case "union":
return undefined;
default:
assertNever(declaration.shape);
}
break;
}
case "container": {
const container = typeReference.container;
switch (container.type) {
case "optional":
return this.maybeUndiscriminatedUnion(container.optional);
case "nullable":
return this.maybeUndiscriminatedUnion(container.nullable);
case "list":
case "set":
case "literal":
case "map":
return undefined;
default:
assertNever(container);
}
break;
}
case "primitive":
case "unknown":
return undefined;
default:
assertNever(typeReference);
}
}

public maybePrimitive(typeReference: FernIr.TypeReference): FernIr.PrimitiveTypeV1 | undefined {
switch (typeReference.type) {
case "container": {
Expand Down
21 changes: 21 additions & 0 deletions generators/go-v2/sdk/src/endpoint/http/HttpEndpointGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,27 @@ export class HttpEndpointGenerator extends AbstractEndpointGenerator {
}
const headerNameVal = header.name;
const headerField = `${this.getRequestParameterName({ endpoint })}.${this.context.getFieldName(headerNameVal)}`;
// Undiscriminated unions are added to the header as a string via their
// generated String() method, since the union struct itself isn't a string.
if (this.context.maybeUndiscriminatedUnion(header.valueType) != null) {
const isOptional = this.context.maybeUnwrapOptionalOrNullable(header.valueType) != null;
const headerValue = this.addHeaderValue({
wireValue: getWireValue(header.name),
value: go.codeblock(`${headerField}.String()`)
});
if (isOptional) {
writer.writeNewLineIfLastLineNot();
writer.writeLine(`if ${headerField} != nil {`);
writer.indent();
writer.writeNode(headerValue);
writer.newLine();
writer.dedent();
writer.writeLine("}");
} else {
writer.writeNode(headerValue);
}
continue;
}
const format = this.context.goValueFormatter.convert({
reference: header.valueType,
value: go.codeblock(headerField)
Expand Down
14 changes: 9 additions & 5 deletions generators/go-v2/sdk/src/endpoint/utils/getPaginationInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,12 +834,16 @@ function getResponseElementType({
context: SdkGeneratorContext;
pagination: FernIr.Pagination;
}): go.Type {
const converted = context.goTypeMapper.convert({ reference: pagination.results.property.valueType });
const iterableElement = converted.underlying().iterableElement();
if (iterableElement != null) {
return iterableElement;
// Resolve the iterable element at the IR level so named aliases to a
// list/set (e.g. `UserList = list<User>`) unwrap to their element type.
// This must match getPaginationValueType, which produces the endpoint's
// returned *core.Page[...] element type, so the pager's PageResponse and
// the endpoint signature agree on the same element type.
const iterableType = context.maybeUnwrapIterable(pagination.results.property.valueType);
if (iterableType != null) {
return context.goTypeMapper.convert({ reference: iterableType });
}
return converted;
return context.goTypeMapper.convert({ reference: pagination.results.property.valueType });
}

function getPagePropertyReference({
Expand Down
5 changes: 5 additions & 0 deletions generators/go/internal/generator/file_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ type fileWriter struct {
// EncodeQueryValues method generated; populated by generateModelTypes.
queryReachableUnions map[common.TypeId]struct{}

// headerReachableUnions is the set of undiscriminated union TypeIds that
// may be sent as request headers. Only unions in this set get a String
// method generated; populated by generateModelTypes.
headerReachableUnions map[common.TypeId]struct{}

buffer *bytes.Buffer

// testData collects information about types that need getter/setter tests
Expand Down
33 changes: 26 additions & 7 deletions generators/go/internal/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ func (g *Generator) Generate(mode Mode) ([]*File, error) {

func (g *Generator) generateModelTypes(ir *fernir.IntermediateRepresentation, mode Mode, rootClientInstantiation *ast.AssignStmt, rootPackageName string) ([]*File, []*GeneratedClient, error) {
queryReachableUnions := collectQueryReachableUnions(ir)
headerReachableUnions := collectHeaderReachableUnions(ir)
fileInfoToTypes, err := fileInfoToTypes(
rootPackageName,
ir.Types,
Expand Down Expand Up @@ -192,6 +193,7 @@ func (g *Generator) generateModelTypes(ir *fernir.IntermediateRepresentation, mo
g.coordinator,
)
writer.queryReachableUnions = queryReachableUnions
writer.headerReachableUnions = headerReachableUnions
for _, typeToGenerate := range typesToGenerate {
switch {
case typeToGenerate.TypeDeclaration != nil:
Expand Down Expand Up @@ -2127,14 +2129,31 @@ func collectQueryReachableUnions(ir *fernir.IntermediateRepresentation) map[comm
for _, service := range ir.Services {
for _, endpoint := range service.Endpoints {
for _, queryParameter := range endpoint.QueryParameters {
walkQueryReachableType(queryParameter.ValueType, ir.Types, reachable, visited)
walkReachableUnionType(queryParameter.ValueType, ir.Types, reachable, visited)
}
}
}
return reachable
}

func walkQueryReachableType(
// collectHeaderReachableUnions returns the set of undiscriminated union TypeIds
// that are reachable from a request header position. Endpoint headers are
// serialized to a string when added to the http.Header, so we generate a String
// method on the unions that may actually be sent as headers.
func collectHeaderReachableUnions(ir *fernir.IntermediateRepresentation) map[common.TypeId]struct{} {
reachable := make(map[common.TypeId]struct{})
visited := make(map[common.TypeId]struct{})
for _, service := range ir.Services {
for _, endpoint := range service.Endpoints {
for _, header := range endpoint.Headers {
walkReachableUnionType(header.ValueType, ir.Types, reachable, visited)
}
}
}
return reachable
}

func walkReachableUnionType(
typeReference *fernir.TypeReference,
types map[common.TypeId]*fernir.TypeDeclaration,
reachable map[common.TypeId]struct{},
Expand All @@ -2146,13 +2165,13 @@ func walkQueryReachableType(
if container := typeReference.Container; container != nil {
switch {
case container.List != nil:
walkQueryReachableType(container.List, types, reachable, visited)
walkReachableUnionType(container.List, types, reachable, visited)
case container.Set != nil:
walkQueryReachableType(container.Set, types, reachable, visited)
walkReachableUnionType(container.Set, types, reachable, visited)
case container.Optional != nil:
walkQueryReachableType(container.Optional, types, reachable, visited)
walkReachableUnionType(container.Optional, types, reachable, visited)
case container.Nullable != nil:
walkQueryReachableType(container.Nullable, types, reachable, visited)
walkReachableUnionType(container.Nullable, types, reachable, visited)
}
return
}
Expand All @@ -2172,6 +2191,6 @@ func walkQueryReachableType(
case declaration.Shape.UndiscriminatedUnion != nil:
reachable[named.TypeId] = struct{}{}
case declaration.Shape.Alias != nil:
walkQueryReachableType(declaration.Shape.Alias.AliasOf, types, reachable, visited)
walkReachableUnionType(declaration.Shape.Alias.AliasOf, types, reachable, visited)
}
}
40 changes: 40 additions & 0 deletions generators/go/internal/generator/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,46 @@ func (t *typeVisitor) VisitUndiscriminatedUnion(union *ir.UndiscriminatedUnionTy
t.writer.P()
}

// Implement fmt.Stringer so undiscriminated unions can be sent as request
// headers. Header values are added to the http.Header as strings, so without
// this the generated client passes the union struct where a string is
// expected and fails to compile. Only emit on unions reachable from a header
// position to avoid bloating unrelated types.
if _, headerReachable := t.writer.headerReachableUnions[t.typeId]; headerReachable {
t.writer.P("func (", receiver, " *", t.typeName, ") String() string {")
t.writer.P("if ", receiver, " == nil {")
t.writer.P(`return ""`)
t.writer.P("}")
for _, member := range members {
if member.isLiteral {
// Literals are constants; the server doesn't need them echoed back.
continue
}
field := fmt.Sprintf("%s.%s", receiver, member.field)
if member.date != nil && !member.isOptional {
t.writer.P(fmt.Sprintf("if %s.typ == %q || !%s.IsZero() {", receiver, member.field, field))
} else {
t.writer.P(fmt.Sprintf("if %s.typ == %q || %s != %s {", receiver, member.field, field, member.zeroValue))
}
if member.date != nil {
// Format date/datetime values using standard layouts so that
// HTTP header consumers receive RFC 3339 / ISO 8601 strings
// rather than Go's default time.Time.String() representation.
if member.date.IsDateTime {
t.writer.P(fmt.Sprintf("return %s.Format(time.RFC3339)", field))
} else {
t.writer.P(fmt.Sprintf(`return %s.Format("2006-01-02")`, field))
}
} else {
t.writer.P(`return fmt.Sprintf("%v", `, field, ")")
}
t.writer.P("}")
}
t.writer.P(`return ""`)
t.writer.P("}")
t.writer.P()
}

// Generate the Visitor interface.
t.writer.P("type ", t.typeName, "Visitor interface {")
for _, member := range members {
Expand Down
29 changes: 11 additions & 18 deletions generators/go/internal/generator/sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -2198,7 +2198,7 @@ func (f *fileWriter) getPaginationInfo(
// TODO: Add support for body property pagination.
return nil, nil
}
resultsSingleType, err := singleTypeReferenceFromResponseProperty(pagination.Cursor.Results)
resultsSingleType, err := singleTypeReferenceFromResponseProperty(pagination.Cursor.Results, f.types)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -2236,7 +2236,7 @@ func (f *fileWriter) getPaginationInfo(
// TODO: Add support for body property pagination.
return nil, nil
}
resultsSingleType, err := singleTypeReferenceFromResponseProperty(pagination.Offset.Results)
resultsSingleType, err := singleTypeReferenceFromResponseProperty(pagination.Offset.Results, f.types)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -2302,27 +2302,20 @@ func nameAndWireValueFromRequestPropertyValue(requestPropertyValue *ir.RequestPr
return nil
}

func singleTypeReferenceFromResponseProperty(responseProperty *ir.ResponseProperty) (*ir.TypeReference, error) {
func singleTypeReferenceFromResponseProperty(responseProperty *ir.ResponseProperty, types map[common.TypeId]*ir.TypeDeclaration) (*ir.TypeReference, error) {
if responseProperty == nil {
return nil, nil
}
property := responseProperty.Property
if property != nil && property.ValueType != nil {
valueType := property.ValueType
optionalOrNullableContainer := getOptionalOrNullableContainer(property.ValueType)
if optionalOrNullableContainer != nil {
valueType = optionalOrNullableContainer
}
switch valueType.Type {
case "container":
switch valueType.Container.Type {
case "list":
return valueType.Container.List, nil
case "set":
return valueType.Container.Set, nil
}
}
return nil, fmt.Errorf("unsupported pagination results type %q", valueType.Type)
// The results property may be a list/set directly, an optional/nullable
// wrapper around one, or a named alias that resolves to one.
// maybeIterableType follows alias indirection and unwraps
// optional/nullable containers before extracting the element type.
if singleType := maybeIterableType(property.ValueType, types); singleType != nil {
return singleType, nil
}
return nil, fmt.Errorf("unsupported pagination results type %q", property.ValueType.Type)
}
return nil, nil
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
- summary: |
Fixed pagination for endpoints whose results property is a named alias to a
list or set (e.g. `results: $response.data` where `data` is a `UserList`
alias). Previously v1 generation aborted with "unsupported pagination
results type", and once that was resolved the generated pager used the alias
itself as the page element type, producing code that did not compile. The
results element type is now resolved through alias indirection (and
optional/nullable wrappers) in both generators, so the pager's element type
matches the endpoint's returned page type.
type: fix
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- summary: |
Fix generated Go failing to compile when an undiscriminated union is sent as
a request header. Such unions now generate a `String()` method (fmt.Stringer)
and the client serializes the header via that method, instead of passing the
union struct where a string is expected.
type: fix
24 changes: 24 additions & 0 deletions generators/go/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 1.45.4
changelogEntry:
- summary: |
Fix generated Go failing to compile when an undiscriminated union is sent as
a request header. Such unions now generate a `String()` method (fmt.Stringer)
and the client serializes the header via that method, instead of passing the
union struct where a string is expected.
type: fix
createdAt: "2026-06-15"
irVersion: 66
- version: 1.45.3
changelogEntry:
- summary: |
Fixed pagination for endpoints whose results property is a named alias to a
list or set (e.g. `results: $response.data` where `data` is a `UserList`
alias). Previously v1 generation aborted with "unsupported pagination
results type", and once that was resolved the generated pager used the alias
itself as the page element type, producing code that did not compile. The
results element type is now resolved through alias indirection (and
optional/nullable wrappers) in both generators, so the pager's element type
matches the endpoint's returned page type.
type: fix
createdAt: "2026-06-15"
irVersion: 66
- version: 1.45.2
changelogEntry:
- summary: |
Expand Down
Loading
Loading