From b4b9734f3999305f92a71db1ec281c34e5bbcfd3 Mon Sep 17 00:00:00 2001 From: Nelson Susanto Date: Thu, 18 Dec 2025 15:04:16 +1100 Subject: [PATCH 1/2] Add CLAUDE.md --- CLAUDE.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..f03cc9fc --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,103 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +go-octopusdeploy is the official Go API client library for [Octopus Deploy](https://octopus.com/), providing programmatic access to the Octopus REST API. The library is hypermedia-driven, meaning API operations are configured at runtime based on Octopus API responses. + +**Module path:** `github.com/OctopusDeploy/go-octopusdeploy/v2` + +## Common Commands + +```bash +# Build +go build -a -race -v ./... + +# Run all tests +go test -v ./... + +# Run a single test +go test -v ./pkg/accounts -run TestAccountServiceAdd + +# Run tests in a specific package +go test -v ./pkg/accounts/... + +# Vet +go vet -v ./... +``` + +## Integration Tests + +Integration tests require a live Octopus Deploy instance. Create a `.env` file at the repo root: +``` +OCTOPUS_HOST=http://your-octopus-instance-url +OCTOPUS_API_KEY=API-YOURAPIKEY +``` + +For VS Code, add to `.vscode/settings.json`: +```json +{ + "go.testEnvFile": "${workspaceFolder}/.env" +} +``` + +## Architecture + +### Client Structure (`pkg/client/octopusdeploy.go`) +The central `Client` struct aggregates 70+ domain-specific services (e.g., `client.Accounts`, `client.Projects`, `client.Deployments`). Initialize with: +```go +client, err := client.NewClient(nil, apiURL, apiKey, spaceID) +``` + +### Service Pattern (`pkg/services/service.go`) +All API services implement the `IService` interface with standardized CRUD operations: +- `Add`, `GetByID`, `Update`, `DeleteByID` +- Services use URI templates for hypermedia-driven API navigation + +### Package Organization (`pkg/`) +- `pkg/client/` - Main client entry point +- `pkg/services/` - Base service infrastructure +- `pkg/resources/` - Generic response wrappers with pagination (`Resources[T]`) +- `pkg/constants/` - Service names, operations, URI templates +- `pkg/[domain]/` - Domain-specific packages (accounts, projects, deployments, etc.) + +### Error Handling (`internal/errors.go`) +Use factory functions for consistent errors: +- `internal.CreateInvalidParameterError(operation, parameter)` +- `internal.CreateRequiredParameterIsEmptyOrNilError(parameter)` + +## Development Patterns + +### Enums +When adding enum values, regenerate string representations with `enumer`: +```bash +go install github.com/dmarkham/enumer@latest +# From the package directory containing the enum: +enumer -type=FilterType -json -output filter_type_string.go +``` + +### Testing Patterns +Unit tests use `stretchr/testify/require` for assertions. Service tests typically follow this structure: +```go +func createAccountService(t *testing.T) *AccountService { + service := NewAccountService(nil, constants.TestURIAccounts) + require.NotNil(t, service) + return service +} + +func TestAccountServiceGetByID(t *testing.T) { + service := createAccountService(t) + resource, err := service.GetByID("") + require.Equal(t, internal.CreateInvalidParameterError(...), err) + require.Nil(t, resource) +} +``` + +## Commit Guidelines + +Use [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `refactor:`, `test:`, `docs:` + +## Releasing + +Create a git tag in format `v[major].[minor].[patch]` (e.g., `v1.0.0`). Release automation via goreleaser triggers on tag creation. \ No newline at end of file From c915a4bca61c091b8e2b8eae35eb7bd860f9958a Mon Sep 17 00:00:00 2001 From: Nelson Susanto Date: Thu, 18 Dec 2025 15:31:47 +1100 Subject: [PATCH 2/2] Remove ConversionState from MarshalJSON git persistence settings --- pkg/projects/git_persistence_settings.go | 5 --- pkg/projects/git_persistence_settings_test.go | 41 ++++++++++++------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/pkg/projects/git_persistence_settings.go b/pkg/projects/git_persistence_settings.go index 8e149b83..68819509 100644 --- a/pkg/projects/git_persistence_settings.go +++ b/pkg/projects/git_persistence_settings.go @@ -145,7 +145,6 @@ func (p *gitPersistenceSettings) MarshalJSON() ([]byte, error) { ProtectedBranchNamePatterns []string `json:"ProtectedBranchNamePatterns"` URL string `json:"Url,omitempty"` Type PersistenceSettingsType `json:"Type,omitempty"` - ConversionState map[string]interface{} `json:"ConversionState,omitempty"` }{ BasePath: p.BasePath(), Credentials: p.Credential(), @@ -154,10 +153,6 @@ func (p *gitPersistenceSettings) MarshalJSON() ([]byte, error) { ProtectedBranchNamePatterns: protectedBranches, URL: p.URL().String(), Type: p.Type(), - ConversionState: map[string]interface{}{ - "VariablesAreInGit": p.conversionState.VariablesAreInGit, - "RunbooksAreInGit": p.conversionState.RunbooksAreInGit, - }, } return json.Marshal(persistenceSettings) diff --git a/pkg/projects/git_persistence_settings_test.go b/pkg/projects/git_persistence_settings_test.go index 3acb6377..de59a1c6 100644 --- a/pkg/projects/git_persistence_settings_test.go +++ b/pkg/projects/git_persistence_settings_test.go @@ -88,11 +88,7 @@ func TestGitPersistenceSettingsMarshalJSONWithProtectedDefaultBranch(t *testing. "ProtectedBranchNamePatterns": [], "ProtectedDefaultBranch": true, "Type": "%s", - "Url": "%s", - "ConversionState": { - "VariablesAreInGit": false, - "RunbooksAreInGit": false - } + "Url": "%s" }`, basePath, gitCredentialsAsJSON, defaultBranch, projects.PersistenceSettingsTypeVersionControlled, url.String()) gitPersistenceSettings := projects.NewGitPersistenceSettings(basePath, gitCredentials, defaultBranch, protectedBranchNamePatterns, url) @@ -125,11 +121,7 @@ func TestGitPersistenceSettingsMarshalJSONWithProtectedDefaultBranchAsLastItem(t "ProtectedBranchNamePatterns": ["foo"], "ProtectedDefaultBranch": true, "Type": "%s", - "Url": "%s", - "ConversionState": { - "VariablesAreInGit": false, - "RunbooksAreInGit": false - } + "Url": "%s" }`, basePath, gitCredentialsAsJSON, defaultBranch, projects.PersistenceSettingsTypeVersionControlled, url.String()) gitPersistenceSettings := projects.NewGitPersistenceSettings(basePath, gitCredentials, defaultBranch, protectedBranchNamePatterns, url) @@ -163,11 +155,7 @@ func TestGitPersistenceSettingsMarshalJSONWithoutProtectedDefaultBranch(t *testi "ProtectedBranchNamePatterns": ["%s"], "ProtectedDefaultBranch": false, "Type": "%s", - "Url": "%s", - "ConversionState": { - "VariablesAreInGit": false, - "RunbooksAreInGit": false - } + "Url": "%s" }`, basePath, gitCredentialsAsJSON, defaultBranch, protectedBranchName, projects.PersistenceSettingsTypeVersionControlled, url.String()) gitPersistenceSettings := projects.NewGitPersistenceSettings(basePath, gitCredentials, defaultBranch, protectedBranchNamePatterns, url) @@ -178,6 +166,29 @@ func TestGitPersistenceSettingsMarshalJSONWithoutProtectedDefaultBranch(t *testi jsonassert.New(t).Assertf(expectedJson, string(gitPersistenceSettingsAsJSON)) } +func TestGitPersistenceSettingsMarshalJSON_OmitsConversionState(t *testing.T) { + url, err := url.Parse("https://example.com/") + require.NoError(t, err) + + gitPersistenceSettings := projects.NewGitPersistenceSettings( + "base/path", + credentials.NewAnonymous(), + "main", + []string{}, + url, + ) + + jsonBytes, err := json.Marshal(gitPersistenceSettings) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(jsonBytes, &result) + require.NoError(t, err) + + _, hasConversionState := result["ConversionState"] + require.False(t, hasConversionState, "ConversionState should not be present in serialized JSON as it is a read-only field") +} + func TestGitPersistenceSettingsUnmarshalJSONWithoutProtectedDefaultBranch(t *testing.T) { basePath := "" anonymousGitCredential := credentials.NewAnonymous()