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
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ Testing:
go test ./internal/ ./core/ ... # unit tests (offline)
go test ./internal/ -run TestRetrier # single test
go test ./tests/ # integration tests — hit the live API
SG_API_TOKEN=... SG_ORG=... go test ./tests/live/ # live typed-decode check of every read endpoint (skips without credentials)
```

If a test binary aborts on macOS with `dyld: missing LC_UUID load command`, the local Go toolchain is too old for the OS: run with `-ldflags=-linkmode=external` or a newer toolchain (`GOTOOLCHAIN=go1.25.0 go test ...`).

The tests in `tests/sdk_test.go` (and `client/client_test.go`, `workflows/client_test.go`) are integration tests against a live StackGuardian org; they require `SG_API_TOKEN` and `SG_BASE_URL` env vars (default base URL: `https://api.app.stackguardian.io`, auth header format: `"apikey <token>"`). Don't expect them to pass without credentials. Offline unit tests live in `internal/` and `core/`.

## History: Fern generation and the patch workflow
Expand All @@ -37,7 +40,7 @@ Nearly every file carries the header `// This file was auto-generated by Fern fr

- **Root package (`api`)** — all shared API types. `types.go` (~8k lines) holds the bulk; per-resource type files (`workflows.go`, `stacks.go`, `policies.go`, `stack_workflow_runs.go`, etc.) hold request/response types and enums. Also exports helpers: `String()`, `Int()`, `Bool()` pointer helpers (`pointer.go`) and `Optional()` / `Null[T]()` (`optional.go`).
- **`client/`** — the top-level `Client` struct aggregating one sub-client per API resource (`Workflows`, `Stacks`, `WorkflowRuns`, `Policies`, `Connectors`, ...). Built with functional options: `client.NewClient(option.WithApiKey(...), option.WithBaseURL(...))`.
- **Resource subpackages** (`workflows/`, `stacks/`, `workflowgroups/`, `stackworkflowruns/`, ...) — one `Client` each with the CRUD methods for that resource. Methods take `(ctx, org, ..., request, opts...)` and resolve org/group/resource path segments into the endpoint URL.
- **Resource subpackages** (`workflows/`, `stacks/`, `workflowgroups/`, `stackworkflowruns/`, ...) — one `Client` each with the CRUD methods for that resource. Methods take `(ctx, org, ..., request, opts...)` and resolve org/group/resource path segments into the endpoint URL. Newer packages (`statebackends/`, `resources/`, `billing/`, `chats/`) and newer methods in older packages keep their request/response types in a `model.go` inside the subpackage as plain structs (no extra-properties plumbing).
- **`core/`** — public runtime: `Optional[T]`, `APIError`, request options plumbing.
- **`internal/`** — private runtime: `Caller` (HTTP execution), retrier, error decoder, query encoding, extra-properties (un)marshaling.
- **`option/`** — public re-exports of request options (`WithApiKey`, `WithBaseURL`, `WithHTTPClient`, `WithMaxAttempts`).
Expand All @@ -47,3 +50,5 @@ Nearly every file carries the header `// This file was auto-generated by Fern fr
- **`*core.Optional[T]` fields** distinguish *omitted* from *explicit null/empty*: `sggosdk.Optional(value)` sends the value, `sggosdk.Null[T]()` sends an explicit `null`, leaving the field `nil` omits it. Several past fixes converted plain fields to `*core.Optional[T]` so callers can send explicit empty values — preserve this pattern when adding fields.
- **Extra properties**: response types expose fields not in the schema via `GetExtraProperties()` (e.g. `response.Data.GetExtraProperties()["ResourceName"]`).
- **Nested workflow groups**: workflow-group names may contain `/` and must NOT be URL-encoded as one segment — see `escapeSlashesForNestedWorkflowGroup` in `workflowgroups/client.go`. Keep this in mind for any endpoint that takes a `wfGrp` path parameter.
- **SDK ↔ API parity**: the SDK covers every client-callable route in the API repo (`../api/platform_api/api/urls.py`, 226 methods). Deliberately not modelled: the inbound VCS webhook receivers (`webhooks/github/`, `.../webhooks/vcs/` — called by GitHub/GitLab with signature auth), `orgs/<org>/email_service/` (marked internal-only in the API), and `.../run_queued_wfruns/` (its core method no longer exists and the authorizer denies the route). Endpoints that are `@extend_schema(exclude=True)` in the API are still modelled; their doc comments note when they need a user (Cognito) token or a runner token instead of an API key. The API wraps responses as `{"msg": ..., "data": ..., "lastevaluatedkey": ...}` and drops falsy keys; several list endpoints answer 204 with no body, which the SDK handles with `ResponseIsOptional`.
- **Live-validated payload rules** (checked against a real organization on 2026-09-03): secret and role PATCH bodies must include `ResourceName`; workflow create for TERRAFORM/OPENTOFU needs `TerraformConfig`; state backend create needs `Statefiles` (an empty list is fine) and, for `aws_s3`, `StateBackendConfig.auth.integrationId`; resource search needs a non-empty `ResourceTypes`; template artifact endpoints take `NAME:REVISION`; several list endpoints answer 204 with no body (all `List*` methods set `ResponseIsOptional`). `tests/live/` re-checks that every read response decodes into the SDK types with no undeclared fields — run it after touching response types.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ func main() {
```


### Integration tests

`go test ./tests/live/` decodes every read endpoint of a real organization into the SDK's typed responses and fails on undeclared fields. It needs `SG_API_TOKEN` and `SG_ORG` and is skipped otherwise.

### Reporting bugs
If you encounter a bug with the SG SDK for Go we would like to hear about it. Please search the [existing issues](https://github.com/StackGuardian/sg-sdk-go/issues) and see if others are experiencing the same issue before opening a new one.

Expand Down
24 changes: 20 additions & 4 deletions access_management.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ type PatchedApiAccessPatch struct {
OidcTrust *core.Optional[OidcTrust] `json:"OIDCTrust,omitempty" url:"-"`
}

// PatchedRole updates a role. The API requires ResourceName in the body.
type PatchedRole struct {
ResourceName *core.Optional[string] `json:"ResourceName,omitempty" url:"-"`
Description *core.Optional[string] `json:"Description,omitempty" url:"-"`
Expand Down Expand Up @@ -486,6 +487,8 @@ type ApiAccessDataResponse struct {
// Generated API key (only in create/regenerate responses)
ApiKey *string `json:"APIKey,omitempty" url:"APIKey,omitempty"`

Id *string `json:"Id,omitempty" url:"Id,omitempty"`

extraProperties map[string]interface{}
rawJSON json.RawMessage
}
Expand Down Expand Up @@ -720,15 +723,15 @@ func (a *ApiAccessDeleteResponse) String() string {
// Serializer for API Access get response
type ApiAccessGetResponse struct {
// (Deprecated) Use 'data' field. Previously contained API access data.
Msg *ApiAccessDataResponse `json:"msg,omitempty" url:"msg,omitempty"`
Msg *string `json:"msg,omitempty" url:"msg,omitempty"`
// Details of the requested API access
Data *ApiAccessDataResponse `json:"data,omitempty" url:"data,omitempty"`

extraProperties map[string]interface{}
rawJSON json.RawMessage
}

func (a *ApiAccessGetResponse) GetMsg() *ApiAccessDataResponse {
func (a *ApiAccessGetResponse) GetMsg() *string {
if a == nil {
return nil
}
Expand Down Expand Up @@ -777,21 +780,29 @@ func (a *ApiAccessGetResponse) String() string {
// Serializer for API Access list response
type ApiAccessListResponse struct {
// List of API accesses
Msg []*ApiAccessDataResponse `json:"msg,omitempty" url:"msg,omitempty"`
Msg *string `json:"msg,omitempty" url:"msg,omitempty"`
Data []*ApiAccessDataResponse `json:"data,omitempty" url:"data,omitempty"`
// Base64 encoded pagination token for next page
Lastevaluatedkey *string `json:"lastevaluatedkey,omitempty" url:"lastevaluatedkey,omitempty"`

extraProperties map[string]interface{}
rawJSON json.RawMessage
}

func (a *ApiAccessListResponse) GetMsg() []*ApiAccessDataResponse {
func (a *ApiAccessListResponse) GetMsg() *string {
if a == nil {
return nil
}
return a.Msg
}

func (a *ApiAccessListResponse) GetData() []*ApiAccessDataResponse {
if a == nil {
return nil
}
return a.Data
}

func (a *ApiAccessListResponse) GetLastevaluatedkey() *string {
if a == nil {
return nil
Expand Down Expand Up @@ -1152,6 +1163,9 @@ type ListUserItem struct {
LoginMethod string `json:"loginMethod" url:"loginMethod"`
Roles []string `json:"roles,omitempty" url:"roles,omitempty"`

FullUserId *string `json:"fullUserId,omitempty" url:"fullUserId,omitempty"`
Alias *string `json:"alias,omitempty" url:"alias,omitempty"`

extraProperties map[string]interface{}
rawJSON json.RawMessage
}
Expand Down Expand Up @@ -1474,6 +1488,8 @@ type RoleDataResponse struct {
IsActive *IsPublicEnum `json:"IsActive,omitempty" url:"IsActive,omitempty"`
IsArchive IsPublicEnum `json:"IsArchive" url:"IsArchive"`

OrgId *string `json:"OrgId,omitempty" url:"OrgId,omitempty"`

extraProperties map[string]interface{}
rawJSON json.RawMessage
}
Expand Down
Loading