Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2f5a317
feat: add proto definitions for cross-environment operations
markphelps Mar 8, 2026
7beda71
feat: add validation and authorization for cross-env request types
markphelps Mar 8, 2026
736d861
feat: add license manager to environments server for Pro gating
markphelps Mar 8, 2026
bf499bc
feat: implement CopyResource RPC
markphelps Mar 8, 2026
b0339af
feat: implement CopyNamespace RPC
markphelps Mar 8, 2026
7bd687f
feat: implement BulkApplyResources RPC
markphelps Mar 8, 2026
16528da
feat: add copyResource and copyNamespace RTK Query mutations
markphelps Mar 8, 2026
4e5cd62
feat: add environment selector and conflict strategy to copy panel
markphelps Mar 8, 2026
670f23b
feat: use server-side CopyResource RPC for flag/segment copy
markphelps Mar 8, 2026
942068d
fix: only require Pro license for cross-environment copy
markphelps Mar 8, 2026
16d5b69
feat: add SDK methods for cross-environment operations
markphelps Mar 8, 2026
3e84428
test: add integration tests for CopyResource
markphelps Mar 8, 2026
163c969
docs: add CLAUDE.md for SDK maintenance guidance
markphelps Mar 8, 2026
43da968
chore: gitignore
markphelps Mar 8, 2026
63a2725
chore: symlink AGENTS
markphelps Mar 8, 2026
250d666
Merge branch 'v2' into feat/cross-env-operations
markphelps Mar 9, 2026
2d39661
Merge branch 'v2' into feat/cross-env-operations
markphelps Mar 10, 2026
03b4452
fix: avoid empty git commits in CopyResource and CopyNamespace
markphelps Mar 10, 2026
6f3d4ee
refactor(environments): collapse copy-resource API into bulk operations
markphelps Mar 10, 2026
9c2ee70
feat(environments): support bulk apply across environments
markphelps Mar 10, 2026
f019673
fix(environments): handle no-op bulk apply and conflict enum mapping
markphelps Mar 10, 2026
b397123
fix(environments): include 'already exists' in bulk apply conflict er…
markphelps Mar 10, 2026
e94b56f
feat: publish v2 protobufs to buf.io/v2
markphelps Mar 20, 2026
c58a526
feat: continue cross env copy work
markphelps Mar 26, 2026
dbdc40e
Merge branch 'v2' into feat/cross-env-operations
markphelps Mar 27, 2026
2d4a666
fix: address review findings for cross-env operations
markphelps Mar 27, 2026
c50bbe6
Merge branch 'v2' into feat/cross-env-operations
markphelps Mar 27, 2026
394b2a0
feat(ui): add environment matrix tab to flag detail page
markphelps Mar 28, 2026
0c26ff3
style(design): fix dark mode, responsive wrap, and loading skeleton
markphelps Mar 28, 2026
dae7115
chore: gitignore update for beads
markphelps Mar 28, 2026
1cfe5e8
chore: delete deadcode
markphelps Mar 28, 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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,7 @@ go.work
go.work.sum

docs/

.dolt/
*.db
.beads/
157 changes: 157 additions & 0 deletions build/testing/integration/environments/copy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package environments

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.flipt.io/build/testing/integration"
"go.flipt.io/flipt/rpc/flipt/core"
"go.flipt.io/flipt/rpc/v2/environments"
"google.golang.org/protobuf/types/known/anypb"
)

func TestBulkApplyResources_CopyEquivalent(t *testing.T) {
integration.Harness(t, func(t *testing.T, opts integration.TestOpts) {
ctx := context.Background()

envClient := opts.TokenClientV2(t).Environments()

const env = integration.DefaultEnvironment

// Get initial revision.
nl, err := envClient.ListNamespaces(ctx, &environments.ListNamespacesRequest{
EnvironmentKey: env,
})
require.NoError(t, err)
revision := nl.Revision

// Ensure "copy-target" namespace exists.
created, err := envClient.CreateNamespace(ctx, &environments.UpdateNamespaceRequest{
EnvironmentKey: env,
Key: "copy-target",
Name: "Copy Target",
Revision: revision,
})
require.NoError(t, err)
revision = created.Revision

// Create a flag in the default namespace to copy.
flagPayload, err := anypb.New(&core.Flag{
Key: "copy-me",
Name: "Copy Me",
Enabled: true,
})
require.NoError(t, err)

stripAnyTypePrefix(t, flagPayload)

srcFlag, err := envClient.CreateResource(ctx, &environments.UpdateResourceRequest{
EnvironmentKey: env,
NamespaceKey: integration.DefaultNamespace,
Key: "copy-me",
Payload: flagPayload,
Revision: revision,
})
require.NoError(t, err)
revision = srcFlag.Revision

srcResource, err := envClient.GetResource(ctx, &environments.GetResourceRequest{
EnvironmentKey: env,
NamespaceKey: integration.DefaultNamespace,
TypeUrl: "flipt.core.Flag",
Key: "copy-me",
})
require.NoError(t, err)

t.Run("copy to different namespace", func(t *testing.T) {
resp, err := envClient.BulkApplyResources(ctx, &environments.BulkApplyResourcesRequest{
EnvironmentKey: env,
NamespaceKeys: []string{"copy-target"},
Operation: environments.BulkOperation_BULK_OPERATION_CREATE,
TypeUrl: "flipt.core.Flag",
Key: "copy-me",
Payload: srcResource.Resource.Payload,
Revision: revision,
})
require.NoError(t, err)

require.Len(t, resp.Results, 1)
assert.Equal(t, environments.OperationStatus_OPERATION_STATUS_SUCCESS, resp.Results[0].Status)
assert.NotEmpty(t, resp.Revision)

revision = resp.Revision

// Verify the resource exists in the target namespace.
got, err := envClient.GetResource(ctx, &environments.GetResourceRequest{
EnvironmentKey: env,
NamespaceKey: "copy-target",
TypeUrl: "flipt.core.Flag",
Key: "copy-me",
})
require.NoError(t, err)
assert.Equal(t, "copy-me", got.Resource.Key)
})

t.Run("conflict strategy FAIL", func(t *testing.T) {
resp, err := envClient.BulkApplyResources(ctx, &environments.BulkApplyResourcesRequest{
EnvironmentKey: env,
NamespaceKeys: []string{"copy-target"},
Operation: environments.BulkOperation_BULK_OPERATION_CREATE,
TypeUrl: "flipt.core.Flag",
Key: "copy-me",
Payload: srcResource.Resource.Payload,
OnConflict: environments.ConflictStrategy_CONFLICT_STRATEGY_FAIL,
Revision: revision,
})
require.NoError(t, err)
require.Len(t, resp.Results, 1)
assert.Equal(t, environments.OperationStatus_OPERATION_STATUS_FAILED, resp.Results[0].Status)
require.NotNil(t, resp.Results[0].Error)
assert.Contains(t, *resp.Results[0].Error, "already exists")
})

t.Run("conflict strategy SKIP", func(t *testing.T) {
resp, err := envClient.BulkApplyResources(ctx, &environments.BulkApplyResourcesRequest{
EnvironmentKey: env,
NamespaceKeys: []string{"copy-target"},
Operation: environments.BulkOperation_BULK_OPERATION_CREATE,
TypeUrl: "flipt.core.Flag",
Key: "copy-me",
Payload: srcResource.Resource.Payload,
OnConflict: environments.ConflictStrategy_CONFLICT_STRATEGY_SKIP,
Revision: revision,
})
require.NoError(t, err)
require.Len(t, resp.Results, 1)
assert.Equal(t, environments.OperationStatus_OPERATION_STATUS_SKIPPED, resp.Results[0].Status)
revision = resp.Revision
})

t.Run("conflict strategy OVERWRITE", func(t *testing.T) {
resp, err := envClient.BulkApplyResources(ctx, &environments.BulkApplyResourcesRequest{
EnvironmentKey: env,
NamespaceKeys: []string{"copy-target"},
Operation: environments.BulkOperation_BULK_OPERATION_CREATE,
TypeUrl: "flipt.core.Flag",
Key: "copy-me",
Payload: srcResource.Resource.Payload,
OnConflict: environments.ConflictStrategy_CONFLICT_STRATEGY_OVERWRITE,
Revision: revision,
})
require.NoError(t, err)
require.Len(t, resp.Results, 1)
assert.Equal(t, environments.OperationStatus_OPERATION_STATUS_SUCCESS, resp.Results[0].Status)
revision = resp.Revision
})

// Cleanup: delete copy-target namespace.
_, err = envClient.DeleteNamespace(ctx, &environments.DeleteNamespaceRequest{
EnvironmentKey: env,
Key: "copy-target",
Revision: revision,
})
require.NoError(t, err)
})
}
2 changes: 1 addition & 1 deletion internal/cmd/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ func NewGRPCServer(
healthsrv = health.NewServer()
)

envsrv, err := serverenvironments.NewServer(logger, environmentStore)
envsrv, err := serverenvironments.NewServer(logger, licenseManager, environmentStore)
if err != nil {
return nil, fmt.Errorf("building environments server: %w", err)
}
Expand Down
Loading
Loading