Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ contract may still change while cfs is pre-1.0.

## [Unreleased]

## [0.3.1] - 2026-09-24

### Changed

- Renamed the repository and Go module to `cloud-foundry-cli-contexts` for
clearer discovery while preserving the `cfs` command name.
- Context metadata now requires explicit workspace and context identities
instead of accepting the earlier incomplete schema.

## [0.3.0] - 2026-09-23

Expand Down Expand Up @@ -98,7 +102,8 @@ contract may still change while cfs is pre-1.0.
- Linux and macOS release builds, checksums, provenance, CI, smoke tests, and an
Apache-2.0 license.

[Unreleased]: https://github.com/zongqichen/cloud-foundry-cli-contexts/compare/v0.3.0...HEAD
[Unreleased]: https://github.com/zongqichen/cloud-foundry-cli-contexts/compare/v0.3.1...HEAD
[0.3.1]: https://github.com/zongqichen/cloud-foundry-cli-contexts/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/zongqichen/cloud-foundry-cli-contexts/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/zongqichen/cloud-foundry-cli-contexts/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/zongqichen/cloud-foundry-cli-contexts/compare/v0.1.0...v0.1.1
Expand Down
14 changes: 5 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,11 @@ targets.

## Quick start

Install the [official CF CLI](https://github.com/cloudfoundry/cli), then download
`cfs` from [GitHub Releases](https://github.com/zongqichen/cloud-foundry-cli-contexts/releases).
To build the current source with Go 1.26.8+:
Install the [official CF CLI](https://github.com/cloudfoundry/cli), then install
`cfs` with Go 1.26.8+:

```sh
git clone https://github.com/zongqichen/cloud-foundry-cli-contexts.git
cd cloud-foundry-cli-contexts
go install ./cmd/cfs
go install github.com/zongqichen/cloud-foundry-cli-contexts/cmd/cfs@latest
cfs setup
```

Expand Down Expand Up @@ -55,11 +52,10 @@ cfs update

For scripts and coding agents, use `cfs update --json`. When an update is
available, follow the printed command or update through the same installation
channel. A source installation can be updated from its checkout with:
channel. A Go installation can be updated with:

```sh
git pull --ff-only
go install ./cmd/cfs
go install github.com/zongqichen/cloud-foundry-cli-contexts/cmd/cfs@latest
cfs setup
cfs doctor
```
Expand Down
7 changes: 3 additions & 4 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,9 @@ require another login, but it cannot silently attach credentials from an
unrelated directory. A future explicit `cfs rebind` operation may support safe
migration.

The default context ID remains the workspace ID for backward compatibility. A
named context uses a domain-separated SHA-256 digest of the workspace ID and
exact context name. Metadata records both values so renamed or transplanted
state fails validation.
The default context ID is the workspace ID. A named context uses a
domain-separated SHA-256 digest of the workspace ID and exact context name.
Metadata records both values so renamed or transplanted state fails validation.

## 10. State layout

Expand Down
2 changes: 1 addition & 1 deletion internal/app/named_context_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/zongqichen/cloud-foundry-cli-contexts/internal/workspace"
)

func TestNamedContextsUseIndependentHomesAndKeepDefaultCompatible(t *testing.T) {
func TestNamedContextsUseIndependentHomesAndShareDefaultHome(t *testing.T) {
fakeCF := writeFakeCF(t)
stateRoot := canonicalTestPath(t, t.TempDir())
configureTestEnvironment(t, fakeCF, stateRoot)
Expand Down
10 changes: 5 additions & 5 deletions internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ type Context struct {
type Metadata struct {
Version int `json:"version"`
ContextID string `json:"context_id"`
ContextName string `json:"context_name,omitempty"`
WorkspaceID string `json:"workspace_id,omitempty"`
ContextName string `json:"context_name"`
WorkspaceID string `json:"workspace_id"`
Workspace string `json:"workspace"`
Source string `json:"source"`
Fingerprint string `json:"fingerprint"`
Expand Down Expand Up @@ -188,13 +188,13 @@ func (s Store) ReadMetadata(ctx Context) (Metadata, error) {
return Metadata{}, fmt.Errorf("unsupported metadata version %d", metadata.Version)
}
if metadata.ContextName == "" {
metadata.ContextName = contextname.Default
return Metadata{}, errors.New("missing context name in context metadata")
}
if err := contextname.Validate(metadata.ContextName); err != nil {
return Metadata{}, err
}
if metadata.WorkspaceID == "" && metadata.ContextName == contextname.Default {
metadata.WorkspaceID = metadata.ContextID
if metadata.WorkspaceID == "" {
return Metadata{}, errors.New("missing workspace ID in context metadata")
}
if !validID(metadata.WorkspaceID) {
return Metadata{}, errors.New("invalid workspace ID in context metadata")
Expand Down
53 changes: 32 additions & 21 deletions internal/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/zongqichen/cloud-foundry-cli-contexts/internal/workspace"
)

func TestContextForNameKeepsDefaultIdentityAndSeparatesNames(t *testing.T) {
func TestContextForNameUsesWorkspaceIdentityForDefaultAndSeparatesNames(t *testing.T) {
s := New(t.TempDir())
ws := workspace.Workspace{ID: strings.Repeat("a", 64)}

Expand Down Expand Up @@ -109,7 +109,7 @@ func TestEnsureRejectsMetadataMismatch(t *testing.T) {
}
}

func TestReadMetadataAcceptsLegacyDefaultContext(t *testing.T) {
func TestReadMetadataRejectsIncompleteIdentity(t *testing.T) {
root := t.TempDir()
s := New(root)
ws := workspace.Workspace{
Expand All @@ -126,25 +126,36 @@ func TestReadMetadataAcceptsLegacyDefaultContext(t *testing.T) {
if err := s.Prepare(ctx); err != nil {
t.Fatal(err)
}
legacy := map[string]any{
"version": 1, "context_id": ws.ID, "workspace": ws.Root,
"source": ws.Source, "fingerprint": ws.Fingerprint,
"created_at": time.Now().UTC(), "last_used_at": time.Now().UTC(),
}
raw, err := json.Marshal(legacy)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(ctx.MetadataPath, raw, 0o600); err != nil {
t.Fatal(err)
}

metadata, err := s.ReadMetadata(ctx)
if err != nil {
t.Fatal(err)
}
if metadata.ContextName != contextname.Default || metadata.WorkspaceID != ws.ID {
t.Fatalf("legacy metadata was not normalized: %#v", metadata)
metadata := Metadata{
Version: metadataVersion,
ContextID: ctx.ID,
ContextName: contextname.Default,
WorkspaceID: ws.ID,
Workspace: ws.Root,
Source: ws.Source,
Fingerprint: ws.Fingerprint,
CreatedAt: time.Now().UTC(),
LastUsedAt: time.Now().UTC(),
}
tests := []struct {
name string
mutate func(*Metadata)
want string
}{
{name: "context name", mutate: func(value *Metadata) { value.ContextName = "" }, want: "missing context name"},
{name: "workspace ID", mutate: func(value *Metadata) { value.WorkspaceID = "" }, want: "missing workspace ID"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
incomplete := metadata
test.mutate(&incomplete)
if err := os.WriteFile(ctx.MetadataPath, mustJSON(t, incomplete), 0o600); err != nil {
t.Fatal(err)
}
if _, err := s.ReadMetadata(ctx); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("ReadMetadata() error = %v, want %q", err, test.want)
}
})
}
}

Expand Down
Loading